From b3c732bbb71401cad667b7067269f88c45236f1d Mon Sep 17 00:00:00 2001 From: Devin Bidwell Date: Mon, 1 Dec 2025 23:08:56 -0700 Subject: [PATCH] Start generating documentation for built-in types and functions --- rust_compiler/Cargo.lock | 1 + rust_compiler/libs/parser/Cargo.toml | 1 + rust_compiler/libs/parser/src/lib.rs | 5 + rust_compiler/libs/parser/src/macros.rs | 85 ++++++ rust_compiler/libs/parser/src/sys_call.rs | 303 ++++++++++++--------- rust_compiler/libs/parser/src/test/docs.rs | 12 + rust_compiler/libs/parser/src/test/mod.rs | 10 +- 7 files changed, 279 insertions(+), 138 deletions(-) create mode 100644 rust_compiler/libs/parser/src/macros.rs create mode 100644 rust_compiler/libs/parser/src/test/docs.rs diff --git a/rust_compiler/Cargo.lock b/rust_compiler/Cargo.lock index b42d3c4..0c3ea01 100644 --- a/rust_compiler/Cargo.lock +++ b/rust_compiler/Cargo.lock @@ -496,6 +496,7 @@ version = "0.1.0" dependencies = [ "anyhow", "lsp-types", + "pretty_assertions", "quick-error", "tokenizer", ] diff --git a/rust_compiler/libs/parser/Cargo.toml b/rust_compiler/libs/parser/Cargo.toml index 5ff0cd5..504d535 100644 --- a/rust_compiler/libs/parser/Cargo.toml +++ b/rust_compiler/libs/parser/Cargo.toml @@ -11,3 +11,4 @@ lsp-types = { workspace = true } [dev-dependencies] anyhow = { version = "1" } +pretty_assertions = "1.4" diff --git a/rust_compiler/libs/parser/src/lib.rs b/rust_compiler/libs/parser/src/lib.rs index 994e71f..f58560f 100644 --- a/rust_compiler/libs/parser/src/lib.rs +++ b/rust_compiler/libs/parser/src/lib.rs @@ -1,3 +1,4 @@ +mod macros; #[cfg(test)] mod test; @@ -14,6 +15,10 @@ use tokenizer::{ }; use tree_node::*; +pub trait Documentation { + fn docs(&self) -> String; +} + #[macro_export] /// A macro to create a boxed value. macro_rules! boxed { diff --git a/rust_compiler/libs/parser/src/macros.rs b/rust_compiler/libs/parser/src/macros.rs new file mode 100644 index 0000000..6eefb02 --- /dev/null +++ b/rust_compiler/libs/parser/src/macros.rs @@ -0,0 +1,85 @@ +#[macro_export] +macro_rules! documented { + // ------------------------------------------------------------------------- + // Internal Helper: Filter doc comments + // ------------------------------------------------------------------------- + + // Case 1: Doc comment. Return Some("string"). + // We match the specific structure of a doc attribute. + (@doc_filter #[doc = $doc:expr]) => { + Some($doc) + }; + + // Case 2: Other attributes (derives, etc.). Return None. + // We catch any other token sequence inside the brackets. + (@doc_filter #[$($attr:tt)*]) => { + None + }; + + // ------------------------------------------------------------------------- + // Internal Helper: Match patterns for `match self` + // ------------------------------------------------------------------------- + (@arm $name:ident $variant:ident) => { + $name::$variant + }; + (@arm $name:ident $variant:ident ( $($tuple:tt)* )) => { + $name::$variant(..) + }; + (@arm $name:ident $variant:ident { $($structure:tt)* }) => { + $name::$variant{..} + }; + + // ------------------------------------------------------------------------- + // Main Macro Entry Point + // ------------------------------------------------------------------------- + ( + $(#[$enum_attr:meta])* $vis:vis enum $name:ident { + $( + // Capture attributes as a sequence of token trees inside brackets + // to avoid "local ambiguity" and handle multi-token attributes (like doc="..."). + $(#[ $($variant_attr:tt)* ])* + $variant:ident + $( ($($tuple:tt)*) )? + $( {$($structure:tt)*} )? + ),* $(,)? + } + ) => { + // 1. Generate the actual Enum definition + $(#[$enum_attr])* + $vis enum $name { + $( + $(#[ $($variant_attr)* ])* + $variant + $( ($($tuple)*) )? + $( {$($structure)*} )?, + )* + } + + // 2. Implement the Trait + impl Documentation for $name { + fn docs(&self) -> String { + match self { + $( + documented!(@arm $name $variant $( ($($tuple)*) )? $( {$($structure)*} )? ) => { + // Create a temporary array of Option<&str> for all attributes + let doc_lines: &[Option<&str>] = &[ + $( + documented!(@doc_filter #[ $($variant_attr)* ]) + ),* + ]; + + // Filter out the Nones (non-doc attributes), join, and return + doc_lines.iter() + .filter_map(|&d| d) + .collect::>() + .join("\n") + .trim() + .to_string() + } + )* + } + } + } + }; +} + diff --git a/rust_compiler/libs/parser/src/sys_call.rs b/rust_compiler/libs/parser/src/sys_call.rs index 494fac5..ed9d2c9 100644 --- a/rust_compiler/libs/parser/src/sys_call.rs +++ b/rust_compiler/libs/parser/src/sys_call.rs @@ -1,73 +1,107 @@ -use crate::tree_node::{Expression, Literal, Spanned}; - use super::LiteralOrVariable; +use crate::tree_node::{Expression, Literal, Spanned}; +use crate::{Documentation, documented}; -#[derive(Debug, PartialEq, Eq)] -pub enum Math { - /// Returns the angle in radians whose cosine is the specified number. - /// ## In Game - /// `acos r? a(r?|num)` - Acos(LiteralOrVariable), - /// Returns the angle in radians whose sine is the specified number. - /// ## In Game - /// `asin r? a(r?|num)` - Asin(LiteralOrVariable), - /// Returns the angle in radians whose tangent is the specified number. - /// ## In Game - /// `atan r? a(r?|num)` - Atan(LiteralOrVariable), - /// Returns the angle in radians whose tangent is the quotient of the specified numbers. - /// ## In Game - /// `atan2 r? a(r?|num) b(r?|num)` - Atan2(LiteralOrVariable, LiteralOrVariable), - /// Gets the absolute value of a number. - /// ## In Game - /// `abs r? a(r?|num)` - Abs(LiteralOrVariable), - /// Rounds a number up to the nearest whole number. - /// ## In Game - /// `ceil r? a(r?|num)` - Ceil(LiteralOrVariable), - /// Returns the cosine of the specified angle in radians. - /// ## In Game - /// cos r? a(r?|num) - Cos(LiteralOrVariable), - /// Rounds a number down to the nearest whole number. - /// ## In Game - /// `floor r? a(r?|num)` - Floor(LiteralOrVariable), - /// Computes the natural logarithm of a number. - /// ## In Game - /// `log r? a(r?|num)` - Log(LiteralOrVariable), - /// Computes the maximum of two numbers. - /// ## In Game - /// `max r? a(r?|num) b(r?|num)` - Max(LiteralOrVariable, LiteralOrVariable), - /// Computes the minimum of two numbers. - /// ## In Game - /// `min r? a(r?|num) b(r?|num)` - Min(LiteralOrVariable, LiteralOrVariable), - /// Gets a random number between 0 and 1. - /// ## In Game - /// `rand r?` - Rand, - /// Returns the sine of the specified angle in radians. - /// ## In Game - /// `sin r? a(r?|num)` - Sin(LiteralOrVariable), - /// Computes the square root of a number. - /// ## In Game - /// `sqrt r? a(r?|num)` - Sqrt(LiteralOrVariable), - /// Returns the tangent of the specified angle in radians. - /// ## In Game - /// `tan r? a(r?|num)` - Tan(LiteralOrVariable), - /// Truncates a number by removing the decimal portion. - /// ## In Game - /// `trunc r? a(r?|num)` - Trunc(LiteralOrVariable), +documented! { + #[derive(Debug, PartialEq, Eq)] + pub enum Math { + /// Returns the angle in radians whose cosine is the specified number. + /// ## IC10 + /// `acos r? a(r?|num)` + /// ## Slang + /// `(number|var).acos();` + Acos(LiteralOrVariable), + /// Returns the angle in radians whose sine is the specified number. + /// ## IC10 + /// `asin r? a(r?|num)` + /// ## Slang + /// `(number|var).asin();` + Asin(LiteralOrVariable), + /// Returns the angle in radians whose tangent is the specified number. + /// ## IC10 + /// `atan r? a(r?|num)` + /// ## Slang + /// `(number|var).atan();` + Atan(LiteralOrVariable), + /// Returns the angle in radians whose tangent is the quotient of the specified numbers. + /// ## IC10 + /// `atan2 r? a(r?|num) b(r?|num)` + /// ## Slang + /// `(number|var).atan2((number|var));` + Atan2(LiteralOrVariable, LiteralOrVariable), + /// Gets the absolute value of a number. + /// ## IC10 + /// `abs r? a(r?|num)` + /// ## Slang + /// `(number|var).abs();` + Abs(LiteralOrVariable), + /// Rounds a number up to the nearest whole number. + /// ## IC10 + /// `ceil r? a(r?|num)` + /// ## Slang + /// `(number|var).ceil();` + Ceil(LiteralOrVariable), + /// Returns the cosine of the specified angle in radians. + /// ## IC10 + /// `cos r? a(r?|num)` + /// ## Slang + /// `(number|var).cos();` + Cos(LiteralOrVariable), + /// Rounds a number down to the nearest whole number. + /// ## In Game + /// `floor r? a(r?|num)` + /// ## Slang + /// `(number|var).floor();` + Floor(LiteralOrVariable), + /// Computes the natural logarithm of a number. + /// ## IC10 + /// `log r? a(r?|num)` + /// ## Slang + /// `(number|var).log();` + Log(LiteralOrVariable), + /// Computes the maximum of two numbers. + /// ## IC10 + /// `max r? a(r?|num) b(r?|num)` + /// ## Slang + /// `(number|var).max((number|var));` + Max(LiteralOrVariable, LiteralOrVariable), + /// Computes the minimum of two numbers. + /// ## IC10 + /// `min r? a(r?|num) b(r?|num)` + /// ## Slang + /// `(number|var).min((number|var));` + Min(LiteralOrVariable, LiteralOrVariable), + /// Gets a random number between 0 and 1. + /// ## IC10 + /// `rand r?` + /// ## Slang + /// `rand();` + Rand, + /// Returns the sine of the specified angle in radians. + /// ## IC10 + /// `sin r? a(r?|num)` + /// ## Slang + /// `(number|var).sin();` + Sin(LiteralOrVariable), + /// Computes the square root of a number. + /// ## IC10 + /// `sqrt r? a(r?|num)` + /// ## Slang + /// `(number|var).sqrt();` + Sqrt(LiteralOrVariable), + /// Returns the tangent of the specified angle in radians. + /// ## IC10 + /// `tan r? a(r?|num)` + /// ## Slang + /// `(number|var).tan();` + Tan(LiteralOrVariable), + /// Truncates a number by removing the decimal portion. + /// ## IC10 + /// `trunc r? a(r?|num)` + /// ## Slang + /// `(number|var).trunc();` + Trunc(LiteralOrVariable), + } } impl std::fmt::Display for Math { @@ -93,71 +127,73 @@ impl std::fmt::Display for Math { } } -#[derive(Debug, PartialEq, Eq)] -pub enum System { - /// Pauses execution for exactly 1 tick and then resumes. - /// ## In Game - /// yield - Yield, - /// Represents a function that can be called to sleep for a certain amount of time. - /// ## In Game - /// `sleep a(r?|num)` - Sleep(Box>), - /// Gets the in-game hash for a specific prefab name. - /// ## In Game - /// `HASH("prefabName")` - Hash(Literal), - /// Represents a function which loads a device variable into a register. - /// ## In Game - /// `l r? d? var` - /// ## Examples - /// `l r0 d0 Setting` - /// `l r1 d5 Pressure` - LoadFromDevice(LiteralOrVariable, Literal), - /// Function which gets a LogicType from all connected network devices that match - /// the provided device hash and name, aggregating them via a batchMode - /// ## In Game - /// lbn r? deviceHash nameHash logicType batchMode - /// ## Examples - /// lbn r0 HASH("StructureWallLight") HASH("wallLight") On Minimum - LoadBatchNamed( - LiteralOrVariable, - Box>, - Literal, - Literal, - ), - /// Loads a LogicType from all connected network devices, aggregating them via a - /// batchMode - /// ## In Game - /// lb r? deviceHash logicType batchMode - /// ## Examples - /// lb r0 HASH("StructureWallLight") On Minimum - LoadBatch(LiteralOrVariable, Literal, Literal), - /// Represents a function which stores a setting into a specific device. - /// ## In Game - /// `s d? logicType r?` - /// ## Example - /// `s d0 Setting r0` - SetOnDevice(LiteralOrVariable, Literal, Box>), - /// Represents a function which stores a setting to all devices that match - /// the given deviceHash - /// ## In Game - /// `sb deviceHash logicType r?` - /// ## Example - /// `sb HASH("Doors") Lock 1` - SetOnDeviceBatched(LiteralOrVariable, Literal, Box>), - /// Represents a function which stores a setting to all devices that match - /// both the given deviceHash AND the given nameHash - /// ## In Game - /// `sbn deviceHash nameHash logicType r?` - /// ## Example - /// `sbn HASH("Doors") HASH("Exterior") Lock 1` - SetOnDeviceBatchedNamed( - LiteralOrVariable, - LiteralOrVariable, - Literal, - Box>, - ), +documented! { + #[derive(Debug, PartialEq, Eq)] + pub enum System { + /// Pauses execution for exactly 1 tick and then resumes. + /// ## In Game + /// yield + Yield, + /// Represents a function that can be called to sleep for a certain amount of time. + /// ## In Game + /// `sleep a(r?|num)` + Sleep(Box>), + /// Gets the in-game hash for a specific prefab name. + /// ## In Game + /// `HASH("prefabName")` + Hash(Literal), + /// Represents a function which loads a device variable into a register. + /// ## In Game + /// `l r? d? var` + /// ## Examples + /// `l r0 d0 Setting` + /// `l r1 d5 Pressure` + LoadFromDevice(LiteralOrVariable, Literal), + /// Function which gets a LogicType from all connected network devices that match + /// the provided device hash and name, aggregating them via a batchMode + /// ## In Game + /// lbn r? deviceHash nameHash logicType batchMode + /// ## Examples + /// lbn r0 HASH("StructureWallLight") HASH("wallLight") On Minimum + LoadBatchNamed( + LiteralOrVariable, + Box>, + Literal, + Literal, + ), + /// Loads a LogicType from all connected network devices, aggregating them via a + /// batchMode + /// ## In Game + /// lb r? deviceHash logicType batchMode + /// ## Examples + /// lb r0 HASH("StructureWallLight") On Minimum + LoadBatch(LiteralOrVariable, Literal, Literal), + /// Represents a function which stores a setting into a specific device. + /// ## In Game + /// `s d? logicType r?` + /// ## Example + /// `s d0 Setting r0` + SetOnDevice(LiteralOrVariable, Literal, Box>), + /// Represents a function which stores a setting to all devices that match + /// the given deviceHash + /// ## In Game + /// `sb deviceHash logicType r?` + /// ## Example + /// `sb HASH("Doors") Lock 1` + SetOnDeviceBatched(LiteralOrVariable, Literal, Box>), + /// Represents a function which stores a setting to all devices that match + /// both the given deviceHash AND the given nameHash + /// ## In Game + /// `sbn deviceHash nameHash logicType r?` + /// ## Example + /// `sbn HASH("Doors") HASH("Exterior") Lock 1` + SetOnDeviceBatchedNamed( + LiteralOrVariable, + LiteralOrVariable, + Literal, + Box>, + ), + } } impl std::fmt::Display for System { @@ -229,4 +265,3 @@ impl SysCall { ) } } - diff --git a/rust_compiler/libs/parser/src/test/docs.rs b/rust_compiler/libs/parser/src/test/docs.rs new file mode 100644 index 0000000..9a442f9 --- /dev/null +++ b/rust_compiler/libs/parser/src/test/docs.rs @@ -0,0 +1,12 @@ +use crate::Documentation; +use crate::sys_call; +use pretty_assertions::assert_eq; + +#[test] +fn test_token_tree_docs() -> anyhow::Result<()> { + let syscall = sys_call::System::Yield; + + assert_eq!(syscall.docs(), ""); + + Ok(()) +} diff --git a/rust_compiler/libs/parser/src/test/mod.rs b/rust_compiler/libs/parser/src/test/mod.rs index c23869a..b7a7177 100644 --- a/rust_compiler/libs/parser/src/test/mod.rs +++ b/rust_compiler/libs/parser/src/test/mod.rs @@ -6,9 +6,11 @@ macro_rules! parser { } mod blocks; +mod docs; use super::Parser; use super::Tokenizer; use anyhow::Result; +use pretty_assertions::assert_eq; #[test] fn test_unsupported_keywords() -> Result<()> { @@ -99,16 +101,16 @@ fn test_priority_expression() -> Result<()> { #[test] fn test_binary_expression() -> Result<()> { - let expr = parser!("4 ** 2 + 5 ** 2").parse()?.unwrap(); + let expr = parser!("4 ** 2 + 5 ** 2;").parse()?.unwrap(); assert_eq!("((4 ** 2) + (5 ** 2))", expr.to_string()); - let expr = parser!("2 ** 3 ** 4").parse()?.unwrap(); + let expr = parser!("2 ** 3 ** 4;").parse()?.unwrap(); assert_eq!("(2 ** (3 ** 4))", expr.to_string()); - let expr = parser!("45 * 2 - 15 / 5 + 5 ** 2").parse()?.unwrap(); + let expr = parser!("45 * 2 - 15 / 5 + 5 ** 2;").parse()?.unwrap(); assert_eq!("(((45 * 2) - (15 / 5)) + (5 ** 2))", expr.to_string()); - let expr = parser!("(5 - 2) * 10").parse()?.unwrap(); + let expr = parser!("(5 - 2) * 10;").parse()?.unwrap(); assert_eq!("((5 - 2) * 10)", expr.to_string()); Ok(())