Skip to content

Commit

Permalink
Support a catch all field in tagged enums (#66)
Browse files Browse the repository at this point in the history
Co-authored-by: Jack <[email protected]>
  • Loading branch information
sasial-dev and jackdotink authored Feb 9, 2024
1 parent 6de12fa commit bb80746
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 17 deletions.
31 changes: 31 additions & 0 deletions docs/config/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ const enumExample = `enum "Type" {
},
}`

const enumCatchAllExample = `event UpdateStore = {
from: Server,
type: Reliable,
call: SingleSync,
data: enum "name" {
UpdateItem {
arguments: string[2]
},
SetPosition {
arguments: Vector3[1]
},
... {
value: unknown[]
}
}
}`

const structExample = `type Item = struct {
name: string,
price: u16,
Expand Down Expand Up @@ -154,6 +171,10 @@ type t = { type: "number", value: number }

Tagged enums allow you to pass different data depending on a variant. They are extremely powerful and can be used to represent many different types of data.

Tagged enums also have a catch-all clause, for when you want to have optimisation paths for your data, but aren't always sure what shape it is. The an example usecase for a catch-all clause, is serialising [reflex](https://littensy.github.io/reflex/) state from a broadcaster:

<CodeBlock :code="enumCatchAllExample" />

## Structs

Structs are similar to Interfaces, and are a collection of statically named fields with different types.
Expand All @@ -180,6 +201,16 @@ You can also specify what kind of instance you want to accept, for example:

Classes that inherit your specified class will be accepted, for example `Part`.

## Unknown

There are times where we do not know the shape that the data will be at runtime, and we'd like to have Roblox serialise it instead of Zap. This is where the `unknown` type comes in, and zap will serialise the value like instances - passing it to Roblox.

::: warning
As the `unknown` type extends every possible type - the value sent may be `nil`.
:::

<CodeBlock code="unknown" />

## CFrames

Zap supports sending CFrames. There are two types of CFrame you may send - a regular `CFrame`, and an `AlignedCFrame`.
Expand Down
1 change: 1 addition & 0 deletions zap/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ pub enum Enum<'src> {
Tagged {
tag: &'src str,
variants: Vec<(&'src str, Struct<'src>)>,
catch_all: Option<Struct<'src>>,
},
}

Expand Down
87 changes: 75 additions & 12 deletions zap/src/output/luau/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,12 @@ pub trait Output {
self.push_dedent_line("end");
}

Enum::Tagged { tag, variants } => {
let numty = NumTy::from_f64(0.0, variants.len() as f64 - 1.0);
Enum::Tagged {
tag,
variants,
catch_all,
} => {
let numty = NumTy::from_f64(0.0, variants.len() as f64);

for (i, (variant_name, variant_struct)) in variants.iter().enumerate() {
if i == 0 {
Expand All @@ -194,15 +198,28 @@ pub trait Output {
}

self.push_line(&format!("alloc({})", numty.size()));
self.push_line(&format!("buffer.write{numty}(outgoing_buff, outgoing_apos, {i})"));
self.push_line(&format!("buffer.write{numty}(outgoing_buff, outgoing_apos, {})", i + 1));

for (field_name, field_ty) in &variant_struct.fields {
self.push_ser(&format!("{from}.{field_name}"), field_ty, checks);
}
}

self.push_dedent_line_indent("else");
self.push_line("error(\"invalid variant value\")");

if let Some(catch_all) = catch_all {
self.push_line(&format!("alloc({})", numty.size()));
self.push_line(&format!("buffer.write{numty}(outgoing_buff, outgoing_apos, 0)"));

self.push_ser(&format!("{from}.{tag}"), &Ty::Str(Range::default()), checks);

for (field_name, field_ty) in &catch_all.fields {
self.push_ser(&format!("{from}.{field_name}"), field_ty, checks);
}
} else {
self.push_line("error(\"invalid variant value\")");
}

self.push_dedent_line("end");
}
},
Expand Down Expand Up @@ -409,8 +426,12 @@ pub trait Output {
self.push_dedent_line("end");
}

Enum::Tagged { tag, variants } => {
let numty = NumTy::from_f64(0.0, variants.len() as f64 - 1.0);
Enum::Tagged {
tag,
variants,
catch_all,
} => {
let numty = NumTy::from_f64(0.0, variants.len() as f64);

self.push_line(&format!("{into} = {{}}"));
self.push_line(&format!(
Expand All @@ -420,9 +441,9 @@ pub trait Output {

for (i, (variant_name, variant_struct)) in variants.iter().enumerate() {
if i == 0 {
self.push_line_indent("if enum_index == 0 then");
self.push_line_indent("if enum_index == 1 then");
} else {
self.push_dedent_line_indent(&format!("elseif enum_index == {i} then"));
self.push_dedent_line_indent(&format!("elseif enum_index == {} then", i + 1));
}

self.push_line(&format!("{into}.{tag} = \"{variant_name}\""));
Expand All @@ -433,7 +454,17 @@ pub trait Output {
}

self.push_dedent_line_indent("else");
self.push_line("error(\"unknown enum index\")");

if let Some(catch_all) = catch_all {
self.push_des(&format!("{into}.{tag}"), &Ty::Str(Range::default()), checks);

for (field_name, field_ty) in &catch_all.fields {
self.push_des(&format!("{into}.{field_name}"), field_ty, checks);
}
} else {
self.push_line("error(\"unknown enum index\")");
}

self.push_dedent_line("end");
}
},
Expand Down Expand Up @@ -554,11 +585,18 @@ pub trait Output {
.to_string(),
),

Enum::Tagged { tag, variants } => {
for (i, (name, struct_ty)) in variants.iter().enumerate() {
if i != 0 {
Enum::Tagged {
tag,
variants,
catch_all,
} => {
let mut first = true;

for (name, struct_ty) in variants.iter() {
if !first {
self.push(" | ");
}
first = false;

self.push("{\n");
self.indent();
Expand All @@ -579,6 +617,31 @@ pub trait Output {
self.push_indent();
self.push("}");
}

if let Some(catch_all) = catch_all {
if !first {
self.push(" | ");
}

self.push("{\n");
self.indent();

self.push_indent();

self.push(&format!("{tag}: string,\n"));

for (name, ty) in catch_all.fields.iter() {
self.push_indent();
self.push(&format!("{name}: "));
self.push_ty(ty);
self.push(",\n");
}

self.dedent();

self.push_indent();
self.push("}");
}
}
},

Expand Down
46 changes: 43 additions & 3 deletions zap/src/output/typescript/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,18 @@ pub trait Output {
.to_string(),
),

Enum::Tagged { tag, variants } => {
for (i, (name, struct_ty)) in variants.iter().enumerate() {
if i != 0 {
Enum::Tagged {
tag,
variants,
catch_all,
} => {
let mut first = true;

for (name, struct_ty) in variants.iter() {
if !first {
self.push(" | ");
}
first = false;

self.push("{\n");
self.indent();
Expand Down Expand Up @@ -160,6 +167,39 @@ pub trait Output {
self.push_indent();
self.push("}");
}

if let Some(catch_all) = catch_all {
if !first {
self.push(" | ");
}

self.push("{\n");
self.indent();

self.push_indent();

self.push(&format!("{tag}: string,\n"));

for (name, ty) in catch_all.fields.iter() {
self.push_indent();
self.push(name);

if let Ty::Opt(ty) = ty {
self.push("?: ");
self.push_ty(ty);
} else {
self.push(": ");
self.push_ty(ty);
}

self.push(",\n");
}

self.dedent();

self.push_indent();
self.push("}");
}
}
},

Expand Down
8 changes: 7 additions & 1 deletion zap/src/parser/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,8 +460,13 @@ impl<'src> Converter<'src> {
Enum::Unit(enumerators.iter().map(|e| e.name).collect())
}

SyntaxEnumKind::Tagged { tag, variants } => {
SyntaxEnumKind::Tagged {
tag,
variants,
catch_all,
} => {
let tag_name = self.str(tag);
let catch_all_struct = catch_all.as_ref().map(|syntax_struct| self.struct_ty(syntax_struct));

if variants.is_empty() {
self.report(Report::AnalyzeEmptyEnum { span });
Expand All @@ -485,6 +490,7 @@ impl<'src> Converter<'src> {
Enum::Tagged {
tag: tag_name,
variants,
catch_all: catch_all_struct,
}
}
}
Expand Down
17 changes: 16 additions & 1 deletion zap/src/parser/grammar.lalrpop
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,22 @@ Enum: SyntaxEnum<'input> = {
EnumKind: SyntaxEnumKind<'input> = {
"{" <enumerators:Comma<Identifier>> "}" => SyntaxEnumKind::Unit(enumerators),

<tag:StrLit> "{" <variants:Comma<(<Identifier> <Struct>)>> "}" => SyntaxEnumKind::Tagged { tag, variants },
<tag:StrLit> "{" <mut variants:(<Identifier> <Struct> ",")*> <lv:(<Identifier> <Struct>)?> "}" =>
SyntaxEnumKind::Tagged {
tag,
variants: match lv {
Some(lv) => { variants.push(lv); variants },
None => variants,
},
catch_all: None
},

<tag:StrLit> "{" <variants:(<Identifier> <Struct> ",")*> <ca:("..." <Struct> ","?)> "}" =>
SyntaxEnumKind::Tagged {
tag,
variants,
catch_all: Some(ca),
},
}

Struct: SyntaxStruct<'input> = {
Expand Down
1 change: 1 addition & 0 deletions zap/src/parser/syntax_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ pub enum SyntaxEnumKind<'src> {
Tagged {
tag: SyntaxStrLit<'src>,
variants: Vec<(SyntaxIdentifier<'src>, SyntaxStruct<'src>)>,
catch_all: Option<SyntaxStruct<'src>>,
},
}

Expand Down

0 comments on commit bb80746

Please sign in to comment.