diff --git a/docs/.vitepress/components/Editor.vue b/docs/.vitepress/components/Editor.vue index 7fea8908..54544992 100644 --- a/docs/.vitepress/components/Editor.vue +++ b/docs/.vitepress/components/Editor.vue @@ -91,12 +91,14 @@ const beforeMount = (monaco: Monaco) => { { open: "[", close: "]" }, { open: "(", close: ")" }, { open: '"', close: '"' }, + { open: "'", close: "'" }, ], surroundingPairs: [ { open: "{", close: "}" }, { open: "[", close: "]" }, { open: "(", close: ")" }, { open: '"', close: '"' }, + { open: "'", close: "'" }, ], }); @@ -112,9 +114,10 @@ const beforeMount = (monaco: Monaco) => { const Calls = ["SingleSync", "SingleAsync", "ManySync", "ManyAsync"] as const; - const Options = ["typescript", "write_checks", "casing", "server_output", "client_output", "manual_event_loop"] as const; + const Options = ["typescript", "write_checks", "casing", "server_output", "client_output", "manual_event_loop", "yield_type", "async_lib"] as const; const Casing = ["PascalCase", "camelCase", "snake_case"].map((value) => `"${value}"`); + const YieldType = ["yield", "future", "promise"].map((value) => `"${value}"`); const setting = [...Locations, ...Brand, ...Calls, ...Casing] as const; @@ -151,6 +154,7 @@ const beforeMount = (monaco: Monaco) => { opt: Options, casing: Casing, + yield_type: YieldType, typescript: Operators, write_checks: Operators, @@ -158,6 +162,7 @@ const beforeMount = (monaco: Monaco) => { output_server: [], output_client: [], + async_lib: [], } as const; monaco.languages.registerTokensProviderFactory("zapConfig", { @@ -292,6 +297,22 @@ const beforeMount = (monaco: Monaco) => { documentation: "Event", range: range, }, + { + label: "funct", + kind: monaco.languages.CompletionItemKind.Snippet, + insertText: [ + "funct ${1} = {", + "\tcall: ${2},", + "\targs: ${3},", + "\trets: ${4},", + "}\n", + ].join("\n"), + insertTextRules: + monaco.languages.CompletionItemInsertTextRule + .InsertAsSnippet, + documentation: "Event", + range: range, + }, ]; return { suggestions }; } else { diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index d007189e..d5ef0983 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -19,6 +19,7 @@ const sidebar = [ { text: 'Options', link: '/config/options' }, { text: 'Types', link: '/config/types' }, { text: 'Events', link: '/config/events' }, + { text: 'Functions', link: '/config/functions' }, ] }, { diff --git a/docs/config/functions.md b/docs/config/functions.md new file mode 100644 index 00000000..97c4dbba --- /dev/null +++ b/docs/config/functions.md @@ -0,0 +1,46 @@ + + +# Functions + +Events are another method of communication where the client can send arguments and have them returned by the server. For security, Zap only supports Client -> Server -> Client functions, not Server -> Client -> Server. + +## Defining Events + +Events are defined in your config file using the `funct` keyword. + + + +As you can see they have three fields. Let's go over them one by one: + +### `call` + +This field determines how the event is listened to on the server. The function will take the `args` as parameters and return `rets`. + +- `Async` events can be listened to by one function, and they are called asynchronously. +- `Sync` events can be listened to by one function, and they are called synchronously. + +::: danger +Synchronous events are not recommended, and should only be used when performance is critical. + +- If a synchronous event callback yields it will cause **undefined and game-breaking behavior**. +- If a synchronous event callback errors it will cause **the packet to be dropped**. + +Use synchronous events with extreme caution. +::: + +### `args` + +This field determines the data that is sent to the server. It can be any [Zap type](./types.md). + +### `rets` + +This field determines the data that is sent back to the client from the server. It can be any [Zap type](./types.md). diff --git a/docs/config/options.md b/docs/config/options.md index 182f374d..fc4779b3 100644 --- a/docs/config/options.md +++ b/docs/config/options.md @@ -4,6 +4,9 @@ opt client_output = "path/to/client/output.lua"` const outputExample = `opt server_output = "./network/client.luau" opt client_output = "src/client/zap.luau"` + +const asyncLibExample = `opt yield_type = "promise" +opt async_lib = "require(game:GetService('ReplicatedStorage').Promise)"` # Options @@ -42,7 +45,7 @@ This option does not change the casing of your event or type names. It only chan ### Default -`PascalCase` +`"PascalCase"` ### Options @@ -98,7 +101,7 @@ When enabled, Zap will generate a `.d.ts` file for the server and client with th ## `manual_event_loop` -This option determines if Zap automatically sends reliable events each Heartbeat. +This option determines if Zap automatically sends reliable events and functions each Heartbeat. When enabled, a `SendEvents` function will be exported from the client and server modules that must be called manually. @@ -140,3 +143,55 @@ Note that Zap uses `RunService.Heartbeat` and a 61 hz rate by default. ### Example + +## `yield_type` + +This option changes the way functions yield in zap. + +### Default + +`"yield"` + +### Options + +::: info +The `"future"` option is not avaliable when `typescript` is enabled. +::: + +- `"yield"` +- `"future"` +- `"promise"` + +### Example + + + +## `async_lib` + +::: info +This option is not required when `yield_type` is set to `yield` +::: + +::: tip WARNING +When using `typescript`, provide the path to the RuntimeLib. +::: + +This option provides the async library to Zap. The option must include a `require` statement, as it will be fed directly into the Luau code. + +When using Futures, you must provide a path to [Future by red-blox](https://github.com/red-blox/Util/tree/main/libs/Future). As of writing, there are no other future libraries for Roblox. + +Zap is also compatible with almost any Promise library. Some common examples are: + +- [Promise by evaera](https://github.com/evaera/roblox-lua-promise/)* +- [Promise by Quenty](https://github.com/Quenty/NevermoreEngine/tree/main/src/promise) +- [Promise by red-blox](https://github.com/red-blox/Util/tree/main/libs/Promise) + +*The default in roblox-ts. + +### Default + +The path is empty. + +### Example + + diff --git a/zap/src/config.rs b/zap/src/config.rs index b15e3da1..b72fe670 100644 --- a/zap/src/config.rs +++ b/zap/src/config.rs @@ -7,6 +7,7 @@ use std::{ pub struct Config<'src> { pub tydecls: Vec>, pub evdecls: Vec>, + pub fndecls: Vec>, pub write_checks: bool, pub typescript: bool, @@ -16,6 +17,8 @@ pub struct Config<'src> { pub client_output: &'src str, pub casing: Casing, + pub yield_type: YieldType, + pub async_lib: &'src str, } impl<'src> Config<'src> { @@ -41,13 +44,47 @@ impl Casing { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum YieldType { + Yield, + Future, + Promise, +} + +impl std::fmt::Display for YieldType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + // do nothing, as yield will never import + YieldType::Yield => Ok(()), + YieldType::Future => write!(f, "Future"), + YieldType::Promise => write!(f, "Promise"), + } + } +} + +#[derive(Debug, Clone)] +pub struct FnDecl<'src> { + pub name: &'src str, + pub call: FnCall, + pub args: Option>, + pub rets: Option>, + pub id: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FnCall { + Async, + Sync, +} + #[derive(Debug, Clone)] pub struct EvDecl<'src> { pub name: &'src str, pub from: EvSource, pub evty: EvType, pub call: EvCall, - pub data: Ty<'src>, + pub data: Option>, + pub id: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -106,7 +143,7 @@ impl<'src> Ty<'src> { /// size of the type in the buffer will be 0 bytes. pub fn size( &self, - tydecls: &HashMap<&'src str, TyDecl<'src>>, + tydecls: &HashMap<&'src str, &Ty<'src>>, recursed: &mut HashSet<&'src str>, ) -> (usize, Option) { match self { @@ -168,7 +205,7 @@ impl<'src> Ty<'src> { let tydecl = tydecls.get(name).unwrap(); - tydecl.ty.size(tydecls, recursed) + tydecl.size(tydecls, recursed) } } @@ -200,7 +237,7 @@ pub enum Enum<'src> { impl<'src> Enum<'src> { pub fn size( &self, - tydecls: &HashMap<&'src str, TyDecl<'src>>, + tydecls: &HashMap<&'src str, &Ty<'src>>, recursed: &mut HashSet<&'src str>, ) -> (usize, Option) { match self { @@ -246,7 +283,7 @@ pub struct Struct<'src> { impl<'src> Struct<'src> { pub fn size( &self, - tydecls: &HashMap<&'src str, TyDecl<'src>>, + tydecls: &HashMap<&'src str, &Ty<'src>>, recursed: &mut HashSet<&'src str>, ) -> (usize, Option) { let mut min = 0; diff --git a/zap/src/output/luau/client.luau b/zap/src/output/luau/client.luau index 183f59f8..32dfa372 100644 --- a/zap/src/output/luau/client.luau +++ b/zap/src/output/luau/client.luau @@ -8,6 +8,4 @@ local unreliable = ReplicatedStorage:WaitForChild("ZAP_UNRELIABLE") assert(reliable:IsA("RemoteEvent"), "Expected ZAP_RELIABLE to be a RemoteEvent") assert(unreliable:IsA("UnreliableRemoteEvent"), "Expected ZAP_UNRELIABLE to be an UnreliableRemoteEvent") -local event_queue: { [number]: { any } } = {} - local time = 0 diff --git a/zap/src/output/luau/client.rs b/zap/src/output/luau/client.rs index 0909276a..72c5b4b4 100644 --- a/zap/src/output/luau/client.rs +++ b/zap/src/output/luau/client.rs @@ -1,5 +1,5 @@ use crate::{ - config::{Config, EvCall, EvDecl, EvSource, EvType, TyDecl}, + config::{Config, EvCall, EvDecl, EvSource, EvType, FnDecl, TyDecl, YieldType}, irgen::{des, ser}, }; @@ -135,7 +135,9 @@ impl<'src> ClientOutput<'src> { )); } - fn push_reliable_callback(&mut self, first: bool, ev: &EvDecl, id: usize) { + fn push_reliable_callback(&mut self, first: bool, ev: &EvDecl) { + let id = ev.id; + self.push_indent(); if first { @@ -153,7 +155,10 @@ impl<'src> ClientOutput<'src> { self.indent(); self.push_line("local value"); - self.push_stmts(&des::gen(&ev.data, "value", true)); + + if let Some(data) = &ev.data { + self.push_stmts(&des::gen(data, "value", true)); + } if ev.call == EvCall::SingleSync || ev.call == EvCall::SingleAsync { self.push_line(&format!("if events[{id}] then")); @@ -184,9 +189,14 @@ impl<'src> ClientOutput<'src> { self.push_line("else"); self.indent(); - self.push_line(&format!("table.insert(event_queue[{id}], value)")); + if ev.data.is_some() { + self.push_line(&format!("table.insert(event_queue[{id}], value)")); + self.push_line(&format!("if #event_queue[{id}] > 64 then")); + } else { + self.push_line(&format!("event_queue[{id}] += 1")); + self.push_line(&format!("if event_queue[{id}] > 16 then")); + } - self.push_line(&format!("if #event_queue[{id}] > 64 then")); self.indent(); self.push_line(&format!( @@ -203,6 +213,47 @@ impl<'src> ClientOutput<'src> { self.dedent(); } + fn push_fn_callback(&mut self, first: bool, fndecl: &FnDecl) { + let id = fndecl.id; + + self.push_indent(); + + if first { + self.push("if "); + } else { + self.push("elseif "); + } + + // push_line is not used here as indent was pushed above + // and we don't want to push it twice, especially after + // the if/elseif + self.push(&format!("id == {id} then")); + self.push("\n"); + + self.indent(); + + self.push_line("local call_id = buffer.readu8(incoming_buff, read(1))"); + + self.push_line("local value"); + + if let Some(data) = &fndecl.rets { + self.push_stmts(&des::gen(data, "value", true)); + } + + match self.config.yield_type { + YieldType::Yield | YieldType::Future => { + self.push_line(&format!("task.spawn(event_queue[{id}][call_id], value)")); + } + YieldType::Promise => { + self.push_line(&format!("event_queue[{id}][call_id](value)")); + } + } + + self.push_line(&format!("event_queue[{id}][call_id] = nil")); + + self.dedent(); + } + fn push_reliable_footer(&mut self) { self.push_line("else"); self.indent(); @@ -220,16 +271,18 @@ impl<'src> ClientOutput<'src> { let mut first = true; - for (i, ev) in self + for evdecl in self .config .evdecls .iter() - .enumerate() - .filter(|(_, ev_decl)| ev_decl.from == EvSource::Server && ev_decl.evty == EvType::Reliable) + .filter(|evdecl| evdecl.from == EvSource::Server && evdecl.evty == EvType::Reliable) { - let id = i + 1; + self.push_reliable_callback(first, evdecl); + first = false; + } - self.push_reliable_callback(first, ev, id); + for fndecl in self.config.fndecls.iter() { + self.push_fn_callback(first, fndecl); first = false; } @@ -251,7 +304,9 @@ impl<'src> ClientOutput<'src> { )); } - fn push_unreliable_callback(&mut self, first: bool, ev: &EvDecl, id: usize) { + fn push_unreliable_callback(&mut self, first: bool, ev: &EvDecl) { + let id = ev.id; + self.push_indent(); if first { @@ -269,7 +324,10 @@ impl<'src> ClientOutput<'src> { self.indent(); self.push_line("local value"); - self.push_stmts(&des::gen(&ev.data, "value", self.config.write_checks)); + + if let Some(data) = &ev.data { + self.push_stmts(&des::gen(data, "value", self.config.write_checks)); + } if ev.call == EvCall::SingleSync || ev.call == EvCall::SingleAsync { self.push_line(&format!("if events[{id}] then")); @@ -300,14 +358,19 @@ impl<'src> ClientOutput<'src> { self.push_line("else"); self.indent(); - self.push_line(&format!("table.insert(event_queue[{id}], value)")); + if ev.data.is_some() { + self.push_line(&format!("table.insert(event_queue[{id}], value)")); + self.push_line(&format!("if #event_queue[{id}] > 64 then")); + } else { + self.push_line(&format!("event_queue[{id}] += 1")); + self.push_line(&format!("if event_queue[{id}] > 64 then")); + } - self.push_line(&format!("if #event_queue[{id}] > 64 then")); self.indent(); self.push_line(&format!( - "warn(`[ZAP] {{#event_queue[{id}]}} events in queue for {}. Did you forget to attach a listener?`)", - ev.name + "warn(`[ZAP] {{#event_queue[{}]}} events in queue for {}. Did you forget to attach a listener?`)", + ev.id, ev.name )); self.dedent(); @@ -334,16 +397,13 @@ impl<'src> ClientOutput<'src> { let mut first = true; - for (i, ev) in self + for ev in self .config .evdecls .iter() - .enumerate() - .filter(|(_, ev_decl)| ev_decl.from == EvSource::Server && ev_decl.evty == EvType::Unreliable) + .filter(|ev_decl| ev_decl.from == EvSource::Server && ev_decl.evty == EvType::Unreliable) { - let id = i + 1; - - self.push_unreliable_callback(first, ev, id); + self.push_unreliable_callback(first, ev); first = false; } @@ -351,42 +411,71 @@ impl<'src> ClientOutput<'src> { } fn push_callback_lists(&mut self) { - self.push_line(&format!("local events = table.create({})", self.config.evdecls.len())); + self.push_line(&format!( + "local events = table.create({})", + self.config.evdecls.len() + self.config.fndecls.len() + )); + self.push_line(&format!( + "local event_queue: {{ [number]: {{ any }} }} = table.create({})", + self.config.evdecls.len() + self.config.fndecls.len() + )); + + if !self.config.fndecls.is_empty() { + self.push_line("local function_call_id = 0"); - for (i, ev_decl) in self + if !self.config.async_lib.is_empty() { + if self.config.typescript { + self.push_line(&format!("local Promise = {}.Promise", self.config.async_lib)) + } else { + self.push_line(&format!("local {} = {}", self.config.yield_type, self.config.async_lib)) + } + } + } + + for evdecl in self .config .evdecls .iter() - .enumerate() - .filter(|(_, ev_decl)| ev_decl.from == EvSource::Server) + .filter(|ev_decl| ev_decl.from == EvSource::Server) { - let id = i + 1; + let id = evdecl.id; - if ev_decl.call == EvCall::ManyAsync || ev_decl.call == EvCall::ManySync { + if evdecl.call == EvCall::ManyAsync || evdecl.call == EvCall::ManySync { self.push_line(&format!("events[{id}] = {{}}")); } - self.push_line(&format!("event_queue[{id}] = {{}}")); + if evdecl.data.is_some() { + self.push_line(&format!("event_queue[{id}] = {{}}")); + } else { + self.push_line(&format!("event_queue[{id}] = 0")); + } + } + + for fndecl in self.config.fndecls.iter() { + self.push_line(&format!("event_queue[{}] = table.create(255)", fndecl.id)); } } fn push_write_event_id(&mut self, id: usize) { - self.push_line(&format!("local pos = alloc({})", self.config.event_id_ty().size())); + self.push_line(&format!("alloc({})", self.config.event_id_ty().size())); self.push_line(&format!( - "buffer.write{}(outgoing_buff, pos, {id})", + "buffer.write{}(outgoing_buff, outgoing_apos, {id})", self.config.event_id_ty() )); } - fn push_return_fire(&mut self, ev: &EvDecl, id: usize) { - let ty = &ev.data; - + fn push_return_fire(&mut self, ev: &EvDecl) { let fire = self.config.casing.with("Fire", "fire", "fire"); let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{fire} = function({value}: ")); - self.push_ty(ty); + self.push(&format!("{fire} = function(")); + + if let Some(data) = &ev.data { + self.push(&format!("{value}: ")); + self.push_ty(data); + } + self.push(")\n"); self.indent(); @@ -395,9 +484,11 @@ impl<'src> ClientOutput<'src> { self.push_line("load_empty()"); } - self.push_write_event_id(id); + self.push_write_event_id(ev.id); - self.push_stmts(&ser::gen(ty, value, self.config.write_checks)); + if let Some(data) = &ev.data { + self.push_stmts(&ser::gen(data, value, self.config.write_checks)); + } if ev.evty == EvType::Unreliable { self.push_line("local buff = buffer.create(outgoing_used)"); @@ -411,105 +502,139 @@ impl<'src> ClientOutput<'src> { } fn push_return_outgoing(&mut self) { - for (i, ev) in self + for ev in self .config .evdecls .iter() - .enumerate() - .filter(|(_, ev_decl)| ev_decl.from == EvSource::Client) + .filter(|ev_decl| ev_decl.from == EvSource::Client) { - let id = i + 1; - self.push_line(&format!("{name} = {{", name = ev.name)); self.indent(); - self.push_return_fire(ev, id); + self.push_return_fire(ev); self.dedent(); self.push_line("},"); } } - fn push_return_setcallback(&mut self, ev: &EvDecl, id: usize) { - let ty = &ev.data; + fn push_return_setcallback(&mut self, ev: &EvDecl) { + let id = ev.id; let set_callback = self.config.casing.with("SetCallback", "setCallback", "set_callback"); let callback = self.config.casing.with("Callback", "callback", "callback"); self.push_indent(); self.push(&format!("{set_callback} = function({callback}: (")); - self.push_ty(ty); + + if let Some(data) = &ev.data { + self.push_ty(data); + } + self.push(") -> ())\n"); self.indent(); self.push_line(&format!("events[{id}] = {callback}")); - self.push_line(&format!("for _, value in event_queue[{id}] do")); - self.indent(); + if ev.data.is_some() { + self.push_line(&format!("for _, value in event_queue[{id}] do")); + self.indent(); + + if ev.call == EvCall::SingleSync { + self.push_line(&format!("{callback}(value)")) + } else { + self.push_line(&format!("task.spawn({callback}, value)")) + } - if ev.call == EvCall::SingleSync { - self.push_line(&format!("{callback}(value)")) + self.dedent(); + self.push_line("end"); + + self.push_line(&format!("event_queue[{id}] = {{}}")); } else { - self.push_line(&format!("task.spawn({callback}, value)")) - } + self.push_line(&format!("for 1, event_queue[{id}] do")); + self.indent(); - self.dedent(); - self.push_line("end"); + if ev.call == EvCall::SingleSync { + self.push_line(&format!("{callback}()")) + } else { + self.push_line(&format!("task.spawn({callback})")) + } - self.push_line(&format!("event_queue[{id}] = {{}}")); + self.dedent(); + self.push_line("end"); + + self.push_line(&format!("event_queue[{id}] = 0")); + } self.dedent(); self.push_line("end,"); } - fn push_return_on(&mut self, ev: &EvDecl, id: usize) { - let ty = &ev.data; + fn push_return_on(&mut self, ev: &EvDecl) { + let id = ev.id; let on = self.config.casing.with("On", "on", "on"); let callback = self.config.casing.with("Callback", "callback", "callback"); self.push_indent(); self.push(&format!("{on} = function({callback}: (")); - self.push_ty(ty); + + if let Some(data) = &ev.data { + self.push_ty(data); + } + self.push(") -> ())\n"); self.indent(); self.push_line(&format!("table.insert(events[{id}], {callback})")); - self.push_line(&format!("for _, value in event_queue[{id}] do")); - self.indent(); + if ev.data.is_some() { + self.push_line(&format!("for _, value in event_queue[{id}] do")); + self.indent(); + + if ev.call == EvCall::ManySync { + self.push_line(&format!("{callback}(value)")) + } else { + self.push_line(&format!("task.spawn({callback}, value)")) + } - if ev.call == EvCall::ManySync { - self.push_line(&format!("{callback}(value)")) + self.dedent(); + self.push_line("end"); + + self.push_line(&format!("event_queue[{id}] = {{}}")); } else { - self.push_line(&format!("task.spawn({callback}, value)")) - } + self.push_line(&format!("for 1, event_queue[{id}] do")); + self.indent(); - self.dedent(); - self.push_line("end"); + if ev.call == EvCall::ManySync { + self.push_line(&format!("{callback}()")) + } else { + self.push_line(&format!("task.spawn({callback})")) + } - self.push_line(&format!("event_queue[{id}] = {{}}")); + self.dedent(); + self.push_line("end"); + + self.push_line(&format!("event_queue[{id}] = 0")); + } self.dedent(); self.push_line("end,"); } pub fn push_return_listen(&mut self) { - for (i, ev) in self + for ev in self .config .evdecls .iter() - .enumerate() - .filter(|(_, ev_decl)| ev_decl.from == EvSource::Server) + .filter(|ev_decl| ev_decl.from == EvSource::Server) { - let id = i + 1; - self.push_line(&format!("{name} = {{", name = ev.name)); self.indent(); match ev.call { - EvCall::SingleSync | EvCall::SingleAsync => self.push_return_setcallback(ev, id), - EvCall::ManySync | EvCall::ManyAsync => self.push_return_on(ev, id), + EvCall::SingleSync | EvCall::SingleAsync => self.push_return_setcallback(ev), + EvCall::ManySync | EvCall::ManyAsync => self.push_return_on(ev), } self.dedent(); @@ -517,6 +642,85 @@ impl<'src> ClientOutput<'src> { } } + fn push_return_functions(&mut self) { + let call = self.config.casing.with("Call", "call", "call"); + let value = self.config.casing.with("Value", "value", "value"); + + for fndecl in self.config.fndecls.iter() { + let id = fndecl.id; + + self.push_line(&format!("{name} = {{", name = fndecl.name)); + self.indent(); + + self.push_indent(); + self.push(&format!("{call} = function(")); + + if let Some(ty) = &fndecl.args { + self.push(&format!("{value}: ")); + self.push_ty(ty); + } + + self.push(")\n"); + self.indent(); + + self.push_write_event_id(fndecl.id); + + self.push_line("function_call_id += 1"); + + self.push_line("function_call_id %= 256"); + + self.push_line(&format!("if event_queue[{id}][function_call_id] then")); + self.indent(); + + self.push_line("function_call_id -= 1"); + self.push_line("error(\"Zap has more than 256 calls awaiting a response, and therefore this packet has been dropped\")"); + + self.dedent(); + self.push_line("end"); + + self.push_line("alloc(1)"); + self.push_line("buffer.writeu8(outgoing_buff, outgoing_apos, function_call_id)"); + + if let Some(data) = &fndecl.args { + self.push_stmts(&ser::gen(data, value, self.config.write_checks)); + } + + match self.config.yield_type { + YieldType::Yield => { + self.push_line(&format!("event_queue[{id}][function_call_id] = coroutine.running()",)); + self.push_line("local value = coroutine.yield()"); + } + YieldType::Future => { + self.push_line("local value = Future.new(function()"); + self.indent(); + + self.push_line(&format!("event_queue[{id}][function_call_id] = coroutine.running()",)); + self.push_line("return coroutine.yield()"); + + self.dedent(); + self.push_line("end)"); + } + YieldType::Promise => { + self.push_line("local value = Promise.new(function(resolve)"); + self.indent(); + + self.push_line(&format!("event_queue[{id}][function_call_id] = resolve")); + + self.dedent(); + self.push_line("end)"); + } + } + + self.push_line("return value"); + + self.dedent(); + self.push_line("end,"); + + self.dedent(); + self.push_line("},"); + } + } + pub fn push_return(&mut self) { self.push_line("return {"); self.indent(); @@ -529,6 +733,7 @@ impl<'src> ClientOutput<'src> { self.push_return_outgoing(); self.push_return_listen(); + self.push_return_functions(); self.dedent(); self.push_line("}"); @@ -537,7 +742,7 @@ impl<'src> ClientOutput<'src> { pub fn output(mut self) -> String { self.push_file_header("Client"); - if self.config.evdecls.is_empty() { + if self.config.evdecls.is_empty() && self.config.fndecls.is_empty() { return self.buf; }; @@ -550,11 +755,12 @@ impl<'src> ClientOutput<'src> { self.push_callback_lists(); - if self - .config - .evdecls - .iter() - .any(|ev| ev.evty == EvType::Reliable && ev.from == EvSource::Server) + if !self.config.fndecls.is_empty() + || self + .config + .evdecls + .iter() + .any(|ev| ev.evty == EvType::Reliable && ev.from == EvSource::Server) { self.push_reliable(); } diff --git a/zap/src/output/luau/server.rs b/zap/src/output/luau/server.rs index bd361d0b..0fa796f3 100644 --- a/zap/src/output/luau/server.rs +++ b/zap/src/output/luau/server.rs @@ -1,5 +1,5 @@ use crate::{ - config::{Config, EvCall, EvDecl, EvSource, EvType, TyDecl}, + config::{Config, EvCall, EvDecl, EvSource, EvType, FnCall, FnDecl, TyDecl}, irgen::{des, ser}, }; @@ -130,7 +130,9 @@ impl<'a> ServerOutput<'a> { )); } - fn push_reliable_callback(&mut self, first: bool, ev: &EvDecl, id: usize) { + fn push_reliable_callback(&mut self, first: bool, ev: &EvDecl) { + let id = ev.id; + self.push_indent(); if first { @@ -148,7 +150,10 @@ impl<'a> ServerOutput<'a> { self.indent(); self.push_line("local value"); - self.push_stmts(&des::gen(&ev.data, "value", true)); + + if let Some(data) = &ev.data { + self.push_stmts(&des::gen(data, "value", true)); + } if ev.call == EvCall::SingleSync || ev.call == EvCall::SingleAsync { self.push_line(&format!("if events[{id}] then")) @@ -171,6 +176,63 @@ impl<'a> ServerOutput<'a> { self.dedent(); } + fn push_fn_callback(&mut self, first: bool, fndecl: &FnDecl) { + let id = fndecl.id; + + self.push_indent(); + + if first { + self.push("if "); + } else { + self.push("elseif "); + } + + self.push(&format!("id == {id} then")); + self.push("\n"); + + self.indent(); + + self.push_line("local call_id = buffer.readu8(buff, read(1))"); + self.push_line("local value"); + + if let Some(data) = &fndecl.args { + self.push_stmts(&des::gen(data, "value", true)); + } + + self.push_line(&format!("if events[{id}] then")); + + self.indent(); + + if fndecl.call == FnCall::Async { + self.push_line("task.spawn(function(player, call_id, value)"); + self.indent(); + } + + self.push_line(&format!("local rets = events[{id}](player, value)")); + + self.push_line("load_player(player)"); + self.push_write_event_id(fndecl.id); + + self.push_line("alloc(1)"); + self.push_line("buffer.writeu8(outgoing_buff, outgoing_apos, call_id)"); + + if let Some(ty) = &fndecl.rets { + self.push_stmts(&ser::gen(ty, "rets", self.config.write_checks)); + } + + self.push_line("player_map[player] = save()"); + + if fndecl.call == FnCall::Async { + self.dedent(); + self.push_line("end, player, call_id, value)"); + } + + self.dedent(); + self.push_line("end"); + + self.dedent(); + } + fn push_reliable_footer(&mut self) { self.push_line("else"); self.indent(); @@ -188,16 +250,18 @@ impl<'a> ServerOutput<'a> { let mut first = true; - for (i, ev) in self + for ev in self .config .evdecls .iter() - .enumerate() - .filter(|(_, ev_decl)| ev_decl.from == EvSource::Client && ev_decl.evty == EvType::Reliable) + .filter(|ev_decl| ev_decl.from == EvSource::Client && ev_decl.evty == EvType::Reliable) { - let id = i + 1; + self.push_reliable_callback(first, ev); + first = false; + } - self.push_reliable_callback(first, ev, id); + for fndecl in self.config.fndecls.iter() { + self.push_fn_callback(first, fndecl); first = false; } @@ -219,7 +283,9 @@ impl<'a> ServerOutput<'a> { )); } - fn push_unreliable_callback(&mut self, first: bool, ev: &EvDecl, id: usize) { + fn push_unreliable_callback(&mut self, first: bool, ev: &EvDecl) { + let id = ev.id; + self.push_indent(); if first { @@ -237,7 +303,10 @@ impl<'a> ServerOutput<'a> { self.indent(); self.push_line("local value"); - self.push_stmts(&des::gen(&ev.data, "value", true)); + + if let Some(data) = &ev.data { + self.push_stmts(&des::gen(data, "value", true)); + } if ev.call == EvCall::SingleSync || ev.call == EvCall::SingleAsync { self.push_line(&format!("if events[{id}] then")) @@ -275,16 +344,13 @@ impl<'a> ServerOutput<'a> { let mut first = true; - for (i, ev) in self + for ev in self .config .evdecls .iter() - .enumerate() - .filter(|(_, ev_decl)| ev_decl.from == EvSource::Client && ev_decl.evty == EvType::Unreliable) + .filter(|ev_decl| ev_decl.from == EvSource::Client && ev_decl.evty == EvType::Unreliable) { - let id = i + 1; - - self.push_unreliable_callback(first, ev, id); + self.push_unreliable_callback(first, ev); first = false; } @@ -292,26 +358,27 @@ impl<'a> ServerOutput<'a> { } fn push_callback_lists(&mut self) { - self.push_line(&format!("local events = table.create({})", self.config.evdecls.len())); + self.push_line(&format!( + "local events = table.create({})", + self.config.evdecls.len() + self.config.fndecls.len() + )); - for (i, _) in self.config.evdecls.iter().enumerate().filter(|(_, ev_decl)| { + for evdecl in self.config.evdecls.iter().filter(|ev_decl| { ev_decl.from == EvSource::Client && matches!(ev_decl.call, EvCall::ManyAsync | EvCall::ManySync) }) { - let id = i + 1; - - self.push_line(&format!("events[{id}] = {{}}")); + self.push_line(&format!("events[{}] = {{}}", evdecl.id)); } } fn push_write_event_id(&mut self, id: usize) { - self.push_line(&format!("local pos = alloc({})", self.config.event_id_ty().size())); + self.push_line(&format!("alloc({})", self.config.event_id_ty().size())); self.push_line(&format!( - "buffer.write{}(outgoing_buff, pos, {id})", + "buffer.write{}(outgoing_buff, outgoing_apos, {id})", self.config.event_id_ty() )); } - fn push_return_fire(&mut self, ev: &EvDecl, id: usize) { + fn push_return_fire(&mut self, ev: &EvDecl) { let ty = &ev.data; let fire = self.config.casing.with("Fire", "fire", "fire"); @@ -319,8 +386,13 @@ impl<'a> ServerOutput<'a> { let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{fire} = function({player}: Player, {value}: ")); - self.push_ty(ty); + self.push(&format!("{fire} = function({player}: Player")); + + if let Some(ty) = ty { + self.push(&format!(", {value}: ")); + self.push_ty(ty); + } + self.push(")\n"); self.indent(); @@ -329,9 +401,11 @@ impl<'a> ServerOutput<'a> { EvType::Unreliable => self.push_line("load_empty()"), } - self.push_write_event_id(id); + self.push_write_event_id(ev.id); - self.push_stmts(&ser::gen(ty, value, self.config.write_checks)); + if let Some(ty) = ty { + self.push_stmts(&ser::gen(ty, value, self.config.write_checks)); + } match ev.evty { EvType::Reliable => self.push_line(&format!("player_map[{player}] = save()")), @@ -346,23 +420,30 @@ impl<'a> ServerOutput<'a> { self.push_line("end,"); } - fn push_return_fire_all(&mut self, ev: &EvDecl, id: usize) { + fn push_return_fire_all(&mut self, ev: &EvDecl) { let ty = &ev.data; let fire_all = self.config.casing.with("FireAll", "fireAll", "fire_all"); let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{fire_all} = function({value}: ")); - self.push_ty(ty); + self.push(&format!("{fire_all} = function(")); + + if let Some(ty) = ty { + self.push(&format!("{value}: ")); + self.push_ty(ty); + } + self.push(")\n"); self.indent(); self.push_line("load_empty()"); - self.push_write_event_id(id); + self.push_write_event_id(ev.id); - self.push_stmts(&ser::gen(ty, value, self.config.write_checks)); + if let Some(ty) = ty { + self.push_stmts(&ser::gen(ty, value, self.config.write_checks)); + } match ev.evty { EvType::Reliable => { @@ -389,7 +470,7 @@ impl<'a> ServerOutput<'a> { self.push_line("end,"); } - fn push_return_fire_except(&mut self, ev: &EvDecl, id: usize) { + fn push_return_fire_except(&mut self, ev: &EvDecl) { let ty = &ev.data; let fire_except = self.config.casing.with("FireExcept", "fireExcept", "fire_except"); @@ -397,16 +478,23 @@ impl<'a> ServerOutput<'a> { let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{fire_except} = function({except}: Player, {value}: ")); - self.push_ty(ty); + self.push(&format!("{fire_except} = function({except}: Player")); + + if let Some(ty) = ty { + self.push(&format!(", {value}: ")); + self.push_ty(ty); + } + self.push(")\n"); self.indent(); self.push_line("load_empty()"); - self.push_write_event_id(id); + self.push_write_event_id(ev.id); - self.push_stmts(&ser::gen(ty, value, self.config.write_checks)); + if let Some(ty) = ty { + self.push_stmts(&ser::gen(ty, value, self.config.write_checks)); + } match ev.evty { EvType::Reliable => { @@ -445,7 +533,7 @@ impl<'a> ServerOutput<'a> { self.push_line("end,"); } - fn push_return_fire_list(&mut self, ev: &EvDecl, id: usize) { + fn push_return_fire_list(&mut self, ev: &EvDecl) { let ty = &ev.data; let fire_list = self.config.casing.with("FireList", "fireList", "fire_list"); @@ -453,16 +541,23 @@ impl<'a> ServerOutput<'a> { let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{fire_list} = function({list}: {{ Player }}, {value}: ")); - self.push_ty(ty); + self.push(&format!("{fire_list} = function({list}: {{ Player }}")); + + if let Some(ty) = ty { + self.push(&format!(", {value}: ")); + self.push_ty(ty); + } + self.push(")\n"); self.indent(); self.push_line("load_empty()"); - self.push_write_event_id(id); + self.push_write_event_id(ev.id); - self.push_stmts(&ser::gen(ty, value, self.config.write_checks)); + if let Some(ty) = ty { + self.push_stmts(&ser::gen(ty, value, self.config.write_checks)); + } match ev.evty { EvType::Reliable => { @@ -494,37 +589,39 @@ impl<'a> ServerOutput<'a> { } fn push_return_outgoing(&mut self) { - for (i, ev) in self + for ev in self .config .evdecls .iter() - .enumerate() - .filter(|(_, ev_decl)| ev_decl.from == EvSource::Server) + .filter(|ev_decl| ev_decl.from == EvSource::Server) { - let id = i + 1; - self.push_line(&format!("{name} = {{", name = ev.name)); self.indent(); - self.push_return_fire(ev, id); - self.push_return_fire_all(ev, id); - self.push_return_fire_except(ev, id); - self.push_return_fire_list(ev, id); + self.push_return_fire(ev); + self.push_return_fire_all(ev); + self.push_return_fire_except(ev); + self.push_return_fire_list(ev); self.dedent(); self.push_line("},"); } } - fn push_return_setcallback(&mut self, ev: &EvDecl, id: usize) { - let ty = &ev.data; + fn push_return_setcallback(&mut self, ev: &EvDecl) { + let id = ev.id; let set_callback = self.config.casing.with("SetCallback", "setCallback", "set_callback"); let callback = self.config.casing.with("Callback", "callback", "callback"); self.push_indent(); - self.push(&format!("{set_callback} = function({callback}: (Player, ")); - self.push_ty(ty); + self.push(&format!("{set_callback} = function({callback}: (Player")); + + if let Some(ty) = &ev.data { + self.push(", "); + self.push_ty(ty); + } + self.push(") -> ())\n"); self.indent(); @@ -534,15 +631,20 @@ impl<'a> ServerOutput<'a> { self.push_line("end,"); } - fn push_return_on(&mut self, ev: &EvDecl, id: usize) { - let ty = &ev.data; + fn push_return_on(&mut self, ev: &EvDecl) { + let id = ev.id; let on = self.config.casing.with("On", "on", "on"); let callback = self.config.casing.with("Callback", "callback", "callback"); self.push_indent(); - self.push(&format!("{on} = function({callback}: (Player, ")); - self.push_ty(ty); + self.push(&format!("{on} = function({callback}: (Player")); + + if let Some(ty) = &ev.data { + self.push(", "); + self.push_ty(ty); + } + self.push(") -> ())\n"); self.indent(); @@ -552,27 +654,63 @@ impl<'a> ServerOutput<'a> { self.push_line("end,"); } + fn push_fn_return(&mut self, fndecl: &FnDecl) { + let id = fndecl.id; + + let set_callback = self.config.casing.with("SetCallback", "setCallback", "set_callback"); + let callback = self.config.casing.with("Callback", "callback", "callback"); + + self.push_indent(); + self.push(&format!("{set_callback} = function({callback}: (Player")); + + if let Some(ty) = &fndecl.args { + self.push(", "); + self.push_ty(ty); + } + + self.push(") -> ("); + + if let Some(ty) = &fndecl.rets { + self.push_ty(ty); + } + + self.push("))\n"); + self.indent(); + + self.push_line(&format!("events[{id}] = {callback}")); + + self.dedent(); + self.push_line("end,"); + } + pub fn push_return_listen(&mut self) { - for (i, ev) in self + for ev in self .config .evdecls .iter() - .enumerate() - .filter(|(_, ev_decl)| ev_decl.from == EvSource::Client) + .filter(|ev_decl| ev_decl.from == EvSource::Client) { - let id = i + 1; - - self.push_line(&format!("{name} = {{", name = ev.name)); + self.push_line(&format!("{} = {{", ev.name)); self.indent(); match ev.call { - EvCall::SingleSync | EvCall::SingleAsync => self.push_return_setcallback(ev, id), - EvCall::ManySync | EvCall::ManyAsync => self.push_return_on(ev, id), + EvCall::SingleSync | EvCall::SingleAsync => self.push_return_setcallback(ev), + EvCall::ManySync | EvCall::ManyAsync => self.push_return_on(ev), } self.dedent(); self.push_line("},"); } + + for fndecl in self.config.fndecls.iter() { + self.push_line(&format!("{} = {{", fndecl.name)); + self.indent(); + + self.push_fn_return(fndecl); + + self.dedent(); + self.push_line("},"); + } } pub fn push_return(&mut self) { @@ -595,7 +733,7 @@ impl<'a> ServerOutput<'a> { pub fn output(mut self) -> String { self.push_file_header("Server"); - if self.config.evdecls.is_empty() { + if self.config.evdecls.is_empty() && self.config.fndecls.is_empty() { return self.buf; }; @@ -608,11 +746,12 @@ impl<'a> ServerOutput<'a> { self.push_callback_lists(); - if self - .config - .evdecls - .iter() - .any(|ev| ev.evty == EvType::Reliable && ev.from == EvSource::Client) + if !self.config.fndecls.is_empty() + || self + .config + .evdecls + .iter() + .any(|ev| ev.evty == EvType::Reliable && ev.from == EvSource::Client) { self.push_reliable(); } diff --git a/zap/src/output/typescript/client.rs b/zap/src/output/typescript/client.rs index 8c7a28f5..ebcf0f46 100644 --- a/zap/src/output/typescript/client.rs +++ b/zap/src/output/typescript/client.rs @@ -1,4 +1,4 @@ -use crate::config::{Config, EvCall, EvSource, TyDecl}; +use crate::config::{Config, EvCall, EvSource, Ty, TyDecl, YieldType}; use super::Output; @@ -72,8 +72,20 @@ impl<'src> ClientOutput<'src> { self.indent(); self.push_indent(); - self.push(&format!("{fire}: ({value}: ")); - self.push_ty(&ev.data); + self.push(&format!("{fire}: (")); + + if let Some(data) = &ev.data { + self.push(value); + + if let Ty::Opt(data) = data { + self.push("?: "); + self.push_ty(data); + } else { + self.push(": "); + self.push_ty(data); + } + } + self.push(") => void\n"); self.dedent(); @@ -102,8 +114,20 @@ impl<'src> ClientOutput<'src> { self.indent(); self.push_indent(); - self.push(&format!("{set_callback}: ({callback}: ({value}: ")); - self.push_ty(&ev.data); + self.push(&format!("{set_callback}: ({callback}: (")); + + if let Some(data) = &ev.data { + self.push(value); + + if let Ty::Opt(data) = data { + self.push("?: "); + self.push_ty(data); + } else { + self.push(": "); + self.push_ty(data); + } + } + self.push(") => void) => void\n"); self.dedent(); @@ -111,15 +135,61 @@ impl<'src> ClientOutput<'src> { } } + fn push_return_functions(&mut self) { + let call = self.config.casing.with("Call", "call", "call"); + let value = self.config.casing.with("Value", "value", "value"); + + for fndecl in self.config.fndecls.iter() { + self.push_line(&format!("export const {}: {{", fndecl.name)); + self.indent(); + + self.push_indent(); + self.push(&format!("{call}: (")); + + if let Some(data) = &fndecl.args { + self.push(value); + + if let Ty::Opt(data) = data { + self.push("?: "); + self.push_ty(data); + } else { + self.push(": "); + self.push_ty(data); + } + } + + self.push(") => "); + + if self.config.yield_type == YieldType::Promise { + self.push("Promise<") + } + + if let Some(data) = &fndecl.rets { + self.push_ty(data); + } else { + self.push("void"); + } + + if self.config.yield_type == YieldType::Promise { + self.push(">") + } + + self.push("\n"); + self.dedent(); + self.push_line("};"); + } + } + pub fn push_return(&mut self) { self.push_return_outgoing(); self.push_return_listen(); + self.push_return_functions(); } pub fn output(mut self) -> String { self.push_file_header("Client"); - if self.config.evdecls.is_empty() { + if self.config.evdecls.is_empty() && self.config.fndecls.is_empty() { return self.buf; }; diff --git a/zap/src/output/typescript/mod.rs b/zap/src/output/typescript/mod.rs index ebb56581..9fc3e69d 100644 --- a/zap/src/output/typescript/mod.rs +++ b/zap/src/output/typescript/mod.rs @@ -96,8 +96,9 @@ pub trait Output { self.push("]>"); } _ => { + self.push("("); self.push_ty(ty); - self.push("[]"); + self.push(")[]"); } }, @@ -141,8 +142,16 @@ pub trait Output { for (name, ty) in struct_ty.fields.iter() { self.push_indent(); - self.push(&format!("{name}: ")); - self.push_ty(ty); + 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"); } @@ -160,8 +169,16 @@ pub trait Output { for (name, ty) in struct_ty.fields.iter() { self.push_indent(); - self.push(&format!("{name}: ")); - self.push_ty(ty); + 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"); } diff --git a/zap/src/output/typescript/server.rs b/zap/src/output/typescript/server.rs index 1104d024..0cace5c1 100644 --- a/zap/src/output/typescript/server.rs +++ b/zap/src/output/typescript/server.rs @@ -1,4 +1,4 @@ -use crate::config::{Config, EvCall, EvDecl, EvSource, TyDecl}; +use crate::config::{Config, EvCall, EvDecl, EvSource, Ty, TyDecl}; use super::Output; @@ -58,53 +58,93 @@ impl<'a> ServerOutput<'a> { } fn push_return_fire(&mut self, ev: &EvDecl) { - let ty = &ev.data; - let fire = self.config.casing.with("Fire", "fire", "fire"); let player = self.config.casing.with("Player", "player", "player"); let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{fire}: ({player}: Player, {value}: ")); - self.push_ty(ty); + self.push(&format!("{fire}: ({player}: Player")); + + if let Some(data) = &ev.data { + self.push(&format!(", {value}")); + + if let Ty::Opt(data) = data { + self.push("?: "); + self.push_ty(data); + } else { + self.push(": "); + self.push_ty(data); + } + } + self.push(") => void\n"); } fn push_return_fire_all(&mut self, ev: &EvDecl) { - let ty = &ev.data; - let fire_all = self.config.casing.with("FireAll", "fireAll", "fire_all"); let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{fire_all}: ({value}: ")); - self.push_ty(ty); + self.push(&format!("{fire_all}: (")); + + if let Some(data) = &ev.data { + self.push(value); + + if let Ty::Opt(data) = data { + self.push("?: "); + self.push_ty(data); + } else { + self.push(": "); + self.push_ty(data); + } + } + self.push(") => void\n"); } fn push_return_fire_except(&mut self, ev: &EvDecl) { - let ty = &ev.data; - let fire_except = self.config.casing.with("FireExcept", "fireExcept", "fire_except"); let except = self.config.casing.with("Except", "except", "except"); let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{fire_except}: ({except}: Player, {value}: ")); - self.push_ty(ty); + self.push(&format!("{fire_except}: ({except}: Player")); + + if let Some(data) = &ev.data { + self.push(&format!(", {value}")); + + if let Ty::Opt(data) = data { + self.push("?: "); + self.push_ty(data); + } else { + self.push(": "); + self.push_ty(data); + } + } + self.push(") => void\n"); } fn push_return_fire_list(&mut self, ev: &EvDecl) { - let ty = &ev.data; - let fire_list = self.config.casing.with("FireList", "fireList", "fire_list"); let list = self.config.casing.with("List", "list", "list"); let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{fire_list}: ({list}: Player[], {value}: ")); - self.push_ty(ty); + self.push(&format!("{fire_list}: ({list}: Player[]")); + + if let Some(data) = &ev.data { + self.push(&format!(", {value}")); + + if let Ty::Opt(data) = data { + self.push("?: "); + self.push_ty(data); + } else { + self.push(": "); + self.push_ty(data); + } + } + self.push(") => void\n"); } @@ -151,8 +191,20 @@ impl<'a> ServerOutput<'a> { let value = self.config.casing.with("Value", "value", "value"); self.push_indent(); - self.push(&format!("{set_callback}: ({callback}: ({player}: Player, {value}: ")); - self.push_ty(&ev.data); + self.push(&format!("{set_callback}: ({callback}: ({player}: Player")); + + if let Some(data) = &ev.data { + self.push(&format!(", {value}")); + + if let Ty::Opt(data) = data { + self.push("?: "); + self.push_ty(data); + } else { + self.push(": "); + self.push_ty(data); + } + } + self.push(") => void) => void\n"); self.dedent(); @@ -160,15 +212,56 @@ impl<'a> ServerOutput<'a> { } } + pub fn push_return_functions(&mut self) { + for fndecl in self.config.fndecls.iter() { + self.push_line(&format!("export const {name}: {{", name = fndecl.name)); + self.indent(); + + let set_callback = self.config.casing.with("SetCallback", "setCallback", "set_callback"); + let callback = self.config.casing.with("Callback", "callback", "callback"); + let player = self.config.casing.with("Player", "player", "player"); + let value = self.config.casing.with("Value", "value", "value"); + + self.push_indent(); + self.push(&format!("{set_callback}: ({callback}: ({player}: Player")); + + if let Some(data) = &fndecl.args { + self.push(&format!(", {value}")); + + if let Ty::Opt(data) = data { + self.push("?: "); + self.push_ty(data); + } else { + self.push(": "); + self.push_ty(data); + } + } + + self.push(") => "); + + if let Some(data) = &fndecl.rets { + self.push_ty(data); + } else { + self.push("void"); + } + + self.push(") => void\n"); + + self.dedent(); + self.push_line("};"); + } + } + pub fn push_return(&mut self) { self.push_return_outgoing(); self.push_return_listen(); + self.push_return_functions(); } pub fn output(mut self) -> String { self.push_file_header("Server"); - if self.config.evdecls.is_empty() { + if self.config.evdecls.is_empty() && self.config.fndecls.is_empty() { return self.buf; }; diff --git a/zap/src/parser/convert.rs b/zap/src/parser/convert.rs index a2eea11d..a5e01be8 100644 --- a/zap/src/parser/convert.rs +++ b/zap/src/parser/convert.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use crate::config::{Casing, Config, Enum, EvDecl, EvType, NumTy, Range, Struct, Ty, TyDecl}; +use crate::config::{Casing, Config, Enum, EvDecl, EvType, FnDecl, NumTy, Range, Struct, Ty, TyDecl, YieldType}; use super::{ reports::{Report, Span}, @@ -9,9 +9,8 @@ use super::{ struct Converter<'src> { config: SyntaxConfig<'src>, - tydecls: HashMap<&'src str, SyntaxTyDecl<'src>>, - evdecls: HashMap<&'src str, SyntaxEvDecl<'src>>, + max_unreliable_size: usize, reports: Vec>, } @@ -19,7 +18,7 @@ struct Converter<'src> { impl<'src> Converter<'src> { fn new(config: SyntaxConfig<'src>) -> Self { let mut tydecls = HashMap::new(); - let mut evdecls = HashMap::new(); + let mut ntdecls = 0; for decl in config.decls.iter() { match decl { @@ -27,17 +26,17 @@ impl<'src> Converter<'src> { tydecls.insert(tydecl.name.name, tydecl.clone()); } - SyntaxDecl::Ev(evdecl) => { - evdecls.insert(evdecl.name.name, evdecl.clone()); - } + SyntaxDecl::Ev(_) | SyntaxDecl::Fn(_) => ntdecls += 1, } } + // We subtract two for the `inst` array. + let max_unreliable_size = 900 - NumTy::from_f64(1.0, ntdecls as f64).size() - 2; + Self { config, - tydecls, - evdecls, + max_unreliable_size, reports: Vec::new(), } @@ -45,25 +44,44 @@ impl<'src> Converter<'src> { fn convert(mut self) -> (Config<'src>, Vec>) { let config = self.config.clone(); - let mut tydecls = HashMap::new(); + + self.check_duplicate_decls(&config.decls); + + let mut tydecls = Vec::new(); + let mut evdecls = Vec::new(); + let mut fndecls = Vec::new(); + + let mut ntdecl_id = 0; for tydecl in config.decls.iter().filter_map(|decl| match decl { SyntaxDecl::Ty(tydecl) => Some(tydecl), _ => None, }) { - tydecls.insert(tydecl.name.name, self.tydecl(tydecl)); + tydecls.push(self.tydecl(tydecl)); } - let mut evdecls = Vec::new(); + let tydecl_hashmap = tydecls + .iter() + .map(|tydecl| (tydecl.name, &tydecl.ty)) + .collect::>(); for evdecl in config.decls.iter().filter_map(|decl| match decl { SyntaxDecl::Ev(evdecl) => Some(evdecl), _ => None, }) { - evdecls.push(self.evdecl(evdecl, &tydecls)); + ntdecl_id += 1; + evdecls.push(self.evdecl(evdecl, ntdecl_id, &tydecl_hashmap)); } - if evdecls.is_empty() { + for fndecl in config.decls.iter().filter_map(|decl| match decl { + SyntaxDecl::Fn(fndecl) => Some(fndecl), + _ => None, + }) { + ntdecl_id += 1; + fndecls.push(self.fndecl(fndecl, ntdecl_id)); + } + + if evdecls.is_empty() && fndecls.is_empty() { self.report(Report::AnalyzeEmptyEvDecls); } @@ -74,26 +92,14 @@ impl<'src> Converter<'src> { let (server_output, ..) = self.str_opt("server_output", "network/server.lua", &config.opts); let (client_output, ..) = self.str_opt("client_output", "network/client.lua", &config.opts); - let casing = match self.str_opt("casing", "PascalCase", &config.opts) { - ("snake_case", ..) => Casing::Snake, - ("camelCase", ..) => Casing::Camel, - ("PascalCase", ..) => Casing::Pascal, - - (_, Some(span)) => { - self.report(Report::AnalyzeInvalidOptValue { - span, - expected: "`snake_case`, `camelCase`, or `PascalCase`", - }); - - Casing::Pascal - } - - _ => unreachable!(), - }; + let casing = self.casing_opt(&config.opts); + let yield_type = self.yield_type_opt(typescript, &config.opts); + let async_lib = self.async_lib(yield_type, &config.opts); let config = Config { - tydecls: tydecls.into_values().collect(), + tydecls, evdecls, + fndecls, write_checks, typescript, @@ -103,11 +109,85 @@ impl<'src> Converter<'src> { client_output, casing, + yield_type, + async_lib, }; (config, self.reports) } + fn async_lib(&mut self, yield_type: YieldType, opts: &[SyntaxOpt<'src>]) -> &'src str { + let (async_lib, async_lib_span) = self.str_opt("async_lib", "", opts); + + if let Some(span) = async_lib_span { + if !async_lib.starts_with("require") { + self.report(Report::AnalyzeInvalidOptValue { + span, + expected: "that `async_lib` path must be a `require` statement", + }); + } else if yield_type == YieldType::Yield { + self.report(Report::AnalyzeInvalidOptValue { + span, + expected: "that `async_lib` cannot be defined when using a `yield_type` of `yield`", + }); + } + } else if async_lib.is_empty() && yield_type != YieldType::Yield { + self.report(Report::AnalyzeMissingOptValue { + expected: "`async_lib`", + required_when: "`yield_type` is set to `promise` or `future`.", + }); + } + + async_lib + } + + fn yield_type_opt(&mut self, typescript: bool, opts: &[SyntaxOpt<'src>]) -> YieldType { + match self.str_opt("yield_type", "yield", opts) { + ("yield", ..) => YieldType::Yield, + ("promise", ..) => YieldType::Promise, + ("future", Some(span)) => { + if typescript { + self.report(Report::AnalyzeInvalidOptValue { + span, + expected: "`yield` or `promise`", + }); + } + + YieldType::Future + } + + (_, Some(span)) => { + self.report(Report::AnalyzeInvalidOptValue { + span, + expected: "`yield`, `future`, or `promise`", + }); + + YieldType::Yield + } + + _ => unreachable!(), + } + } + + fn casing_opt(&mut self, opts: &[SyntaxOpt<'src>]) -> Casing { + match self.str_opt("casing", "PascalCase", opts) { + ("snake_case", ..) => Casing::Snake, + ("camelCase", ..) => Casing::Camel, + ("PascalCase", ..) => Casing::Pascal, + + (_, Some(span)) => { + self.report(Report::AnalyzeInvalidOptValue { + span, + expected: "`snake_case`, `camelCase`, or `PascalCase`", + }); + + Casing::Pascal + } + + _ => unreachable!(), + } + } + fn boolean_opt(&mut self, name: &'static str, default: bool, opts: &[SyntaxOpt<'src>]) -> (bool, Option) { let mut value = default; let mut span = None; @@ -171,32 +251,72 @@ impl<'src> Converter<'src> { (value, span) } - fn evdecl(&mut self, evdecl: &SyntaxEvDecl<'src>, tydecls: &HashMap<&'src str, TyDecl<'src>>) -> EvDecl<'src> { + fn check_duplicate_decls(&mut self, decls: &[SyntaxDecl<'src>]) { + let mut tydecls = HashMap::new(); + let mut ntdecls = HashMap::new(); + + for decl in decls.iter() { + match decl { + SyntaxDecl::Ev(ev) => { + if let Some(prev_span) = ntdecls.insert(ev.name.name, ev.span()) { + self.report(Report::AnalyzeDuplicateDecl { + prev_span, + dup_span: ev.span(), + name: ev.name.name, + }); + } + } + + SyntaxDecl::Fn(fn_) => { + if let Some(prev_span) = ntdecls.insert(fn_.name.name, fn_.span()) { + self.report(Report::AnalyzeDuplicateDecl { + prev_span, + dup_span: fn_.span(), + name: fn_.name.name, + }); + } + } + + SyntaxDecl::Ty(ty) => { + if let Some(prev_span) = tydecls.insert(ty.name.name, ty.span()) { + self.report(Report::AnalyzeDuplicateDecl { + prev_span, + dup_span: ty.span(), + name: ty.name.name, + }); + } + } + } + } + } + + fn evdecl( + &mut self, + evdecl: &SyntaxEvDecl<'src>, + id: usize, + tydecls: &HashMap<&'src str, &Ty<'src>>, + ) -> EvDecl<'src> { let name = evdecl.name.name; let from = evdecl.from; let evty = evdecl.evty; let call = evdecl.call; - let data = self.ty(&evdecl.data); - - if let EvType::Unreliable = evty { - let (min, max) = data.size(tydecls, &mut HashSet::new()); - let event_id_size = NumTy::from_f64(1.0, self.evdecls.len() as f64).size(); + let data = evdecl.data.as_ref().map(|ty| self.ty(ty)); - // We subtract two for the `inst` array. - let max_unreliable_size = 900 - event_id_size - 2; + if data.is_some() && evty == EvType::Unreliable { + let (min, max) = data.as_ref().unwrap().size(tydecls, &mut HashSet::new()); - if min > max_unreliable_size { + if min > self.max_unreliable_size { self.report(Report::AnalyzeOversizeUnreliable { ev_span: evdecl.span(), - ty_span: evdecl.data.span(), - max_size: max_unreliable_size, + ty_span: evdecl.data.as_ref().unwrap().span(), + max_size: self.max_unreliable_size, size: min, }); - } else if !max.is_some_and(|max| max < max_unreliable_size) { + } else if !max.is_some_and(|max| max < self.max_unreliable_size) { self.report(Report::AnalyzePotentiallyOversizeUnreliable { ev_span: evdecl.span(), - ty_span: evdecl.data.span(), - max_size: max_unreliable_size, + ty_span: evdecl.data.as_ref().unwrap().span(), + max_size: self.max_unreliable_size, }); } } @@ -207,6 +327,22 @@ impl<'src> Converter<'src> { evty, call, data, + id, + } + } + + fn fndecl(&mut self, fndecl: &SyntaxFnDecl<'src>, id: usize) -> FnDecl<'src> { + let name = fndecl.name.name; + let call = fndecl.call; + let args = fndecl.args.as_ref().map(|ty| self.ty(ty)); + let rets = fndecl.rets.as_ref().map(|ty| self.ty(ty)); + + FnDecl { + name, + args, + call, + rets, + id, } } @@ -325,7 +461,7 @@ impl<'src> Converter<'src> { } SyntaxEnumKind::Tagged { tag, variants } => { - let tag_name = tag.value; + let tag_name = self.str(tag); let variants = variants .iter() diff --git a/zap/src/parser/grammar.lalrpop b/zap/src/parser/grammar.lalrpop index e60b8234..4ad44d1b 100644 --- a/zap/src/parser/grammar.lalrpop +++ b/zap/src/parser/grammar.lalrpop @@ -1,5 +1,5 @@ use crate::parser::{reports::Report, syntax_tree::*}; -use crate::config::{EvCall, EvSource, EvType, NumTy}; +use crate::config::{EvCall, EvSource, EvType, FnCall, NumTy}; use lalrpop_util::ParseError; @@ -35,16 +35,32 @@ OptValueKind: SyntaxOptValueKind<'input> = { } Decl: SyntaxDecl<'input> = { - => SyntaxDecl::Ev(decl), => SyntaxDecl::Ty(decl), + => SyntaxDecl::Ev(decl), + => SyntaxDecl::Fn(decl), +} + +FnDecl: SyntaxFnDecl<'input> = { + "funct" "=" "{" + "call" ":" + )?> + )?> + ","? + "}" ";"? => SyntaxFnDecl { start, name, call, args, rets, end }, +} + +FnCall: FnCall = { + "Async" => FnCall::Async, + "Sync" => FnCall::Sync, } EvDecl: SyntaxEvDecl<'input> = { "event" "=" "{" - "from" ":" "," - "type" ":" "," - "call" ":" "," - "data" ":" ","? + "from" ":" + "," "type" ":" + "," "call" ":" + )?> + ","? "}" ";"? => SyntaxEvDecl { start, name, from, evty, call, data, end }, } @@ -139,6 +155,7 @@ NumRangeKind: SyntaxRangeKind<'input> = { StrLit: SyntaxStrLit<'input> = { => SyntaxStrLit { start, value, end }, + => SyntaxStrLit { start, value, end }, } IntLit: SyntaxNumLit<'input> = { @@ -170,6 +187,8 @@ Identifier: SyntaxIdentifier<'input> = { "opt" => SyntaxIdentifier { start, name: "opt", end }, "from" => SyntaxIdentifier { start, name: "from", end }, "call" => SyntaxIdentifier { start, name: "call", end }, + "Async" => SyntaxIdentifier { start, name: "Async", end }, + "Sync" => SyntaxIdentifier { start, name: "Sync", end }, "Reliable" => SyntaxIdentifier { start, name: "Reliable", end }, "Unreliable" => SyntaxIdentifier { start, name: "Unreliable", end }, "Server" => SyntaxIdentifier { start, name: "Server", end }, diff --git a/zap/src/parser/reports.rs b/zap/src/parser/reports.rs index 557350e5..a1bf5dad 100644 --- a/zap/src/parser/reports.rs +++ b/zap/src/parser/reports.rs @@ -85,6 +85,17 @@ pub enum Report<'src> { decl_span: Span, use_span: Span, }, + + AnalyzeMissingOptValue { + expected: &'static str, + required_when: &'static str, + }, + + AnalyzeDuplicateDecl { + prev_span: Span, + dup_span: Span, + name: &'src str, + }, } impl<'src> Report<'src> { @@ -109,6 +120,8 @@ impl<'src> Report<'src> { Self::AnalyzeNumOutsideRange { .. } => Severity::Error, Self::AnalyzeInvalidOptionalType { .. } => Severity::Error, Self::AnalyzeUnboundedRecursiveType { .. } => Severity::Error, + Self::AnalyzeMissingOptValue { .. } => Severity::Error, + Self::AnalyzeDuplicateDecl { .. } => Severity::Error, } } @@ -124,7 +137,7 @@ impl<'src> Report<'src> { Self::ParserExtraToken { .. } => "extra token".to_string(), Self::ParserExpectedInt { .. } => "expected integer".to_string(), - Self::AnalyzeEmptyEvDecls => "no event declarations".to_string(), + Self::AnalyzeEmptyEvDecls => "no event or function declarations".to_string(), Self::AnalyzeOversizeUnreliable { .. } => "oversize unreliable".to_string(), Self::AnalyzePotentiallyOversizeUnreliable { .. } => "potentially oversize unreliable".to_string(), Self::AnalyzeInvalidRange { .. } => "invalid range".to_string(), @@ -136,6 +149,8 @@ impl<'src> Report<'src> { Self::AnalyzeNumOutsideRange { .. } => "number outside range".to_string(), Self::AnalyzeInvalidOptionalType { .. } => "invalid optional type".to_string(), Self::AnalyzeUnboundedRecursiveType { .. } => "unbounded recursive type".to_string(), + Self::AnalyzeMissingOptValue { .. } => "missing option expected".to_string(), + Self::AnalyzeDuplicateDecl { name, .. } => format!("duplicate declaration '{}'", name), } } @@ -160,6 +175,8 @@ impl<'src> Report<'src> { Self::AnalyzeNumOutsideRange { .. } => "3010", Self::AnalyzeInvalidOptionalType { .. } => "3011", Self::AnalyzeUnboundedRecursiveType { .. } => "3012", + Self::AnalyzeMissingOptValue { .. } => "3013", + Self::AnalyzeDuplicateDecl { .. } => "3014", } } @@ -244,6 +261,17 @@ impl<'src> Report<'src> { Label::primary((), use_span.clone()).with_message("used recursively here"), ] } + + Self::AnalyzeMissingOptValue { .. } => vec![], + + Self::AnalyzeDuplicateDecl { + prev_span, dup_span, .. + } => { + vec![ + Label::secondary((), prev_span.clone()).with_message("previous declaration"), + Label::primary((), dup_span.clone()).with_message("duplicate declaration"), + ] + } } } @@ -256,7 +284,9 @@ impl<'src> Report<'src> { Self::ParserExtraToken { .. } => None, Self::ParserExpectedInt { .. } => None, - Self::AnalyzeEmptyEvDecls => Some(vec!["add an event declaration to allow zap to output code".to_string()]), + Self::AnalyzeEmptyEvDecls => Some(vec![ + "add an event or function declaration to allow zap to output code".to_string() + ]), Self::AnalyzeOversizeUnreliable { max_size, .. } => Some(vec![ format!("all unreliable events must be under {max_size} bytes in size"), "consider adding a upper limit to any arrays or strings".to_string(), @@ -298,6 +328,13 @@ impl<'src> Report<'src> { "this is an unbounded recursive type".to_string(), "unbounded recursive types cause infinite loops".to_string(), ]), + Self::AnalyzeMissingOptValue { + expected, + required_when, + } => Some(vec![format!( + "the {expected} option should not be empty if {required_when}" + )]), + Self::AnalyzeDuplicateDecl { .. } => None, } } } diff --git a/zap/src/parser/syntax_tree.rs b/zap/src/parser/syntax_tree.rs index 0e6d864b..2f8a21b1 100644 --- a/zap/src/parser/syntax_tree.rs +++ b/zap/src/parser/syntax_tree.rs @@ -1,4 +1,4 @@ -use crate::config::{EvCall, EvSource, EvType, NumTy}; +use crate::config::{EvCall, EvSource, EvType, FnCall, NumTy}; use super::reports::Span; @@ -68,8 +68,25 @@ pub enum SyntaxOptValueKind<'src> { #[derive(Debug, Clone, PartialEq)] pub enum SyntaxDecl<'src> { - Ev(SyntaxEvDecl<'src>), Ty(SyntaxTyDecl<'src>), + Ev(SyntaxEvDecl<'src>), + Fn(SyntaxFnDecl<'src>), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SyntaxFnDecl<'src> { + pub start: usize, + pub name: SyntaxIdentifier<'src>, + pub call: FnCall, + pub args: Option>, + pub rets: Option>, + pub end: usize, +} + +impl<'src> Spanned for SyntaxFnDecl<'src> { + fn span(&self) -> Span { + self.start..self.end + } } #[derive(Debug, Clone, PartialEq)] @@ -79,7 +96,7 @@ pub struct SyntaxEvDecl<'src> { pub from: EvSource, pub evty: EvType, pub call: EvCall, - pub data: SyntaxTy<'src>, + pub data: Option>, pub end: usize, }