diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6bdd948 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,69 @@ +name: CI/CD Pipeline + +# Trigger conditions +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +jobs: + # JOB 1: RUN TESTS + # This runs on every PR and every push to master. + # It validates that the Rust code is correct. + test: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + # 1. Build the Docker Image (Cached) + - name: Build Docker Image + run: docker build -t slang-builder -f Dockerfile.build . + + # 2. Run Rust Tests + # --manifest-path: Point to the nested Cargo.toml + # --workspace: Test all crates (compiler, parser, tokenizer, helpers) + # --all-targets: Test lib, bin, and tests folder (skips doc-tests because they are not valid Rust) + - name: Run Rust Tests + run: | + docker run --rm \ + -u $(id -u):$(id -g) \ + -v "$PWD":/app \ + slang-builder \ + cargo test --manifest-path rust_compiler/Cargo.toml --workspace --all-targets + + build: + needs: test + runs-on: self-hosted + if: github.event_name == 'push' + + steps: + - uses: actions/checkout@v4 + + # 1. Build Image (Fast, uses cache from previous job if available/local) + - name: Build Docker Image + run: docker build -t slang-builder -f Dockerfile.build . + + # 2. Run the Build Script + # Mounts the references from the server's secure storage + - name: Build Release Artifacts + run: | + docker run --rm \ + -u $(id -u):$(id -g) \ + -v "$PWD":/app \ + -v "/home/github-runner/permanent-refs":/app/csharp_mod/refs \ + slang-builder \ + ./build.sh + + # 3. Fix Permissions + # Docker writes files as root. We need to own them to upload them. + - name: Fix Permissions + if: always() + run: sudo chown -R $USER:$USER release/ + + # 4. Upload to GitHub + - name: Upload Release Artifacts + uses: actions/upload-artifact@v4 + with: + name: StationeersSlang-Release + path: release/ diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 0000000..cbb06c5 --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,19 @@ +FROM rust:latest + +RUN apt-get update && apt-get install -y \ + mingw-w64 \ + wget \ + apt-transport-https \ + && rm -rf /var/lib/apt/lists/* + +RUN wget --progress=dot:giga https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \ + && chmod +x ./dotnet-install.sh \ + && ./dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet \ + && ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet + +RUN rustup target add x86_64-pc-windows-gnu && rustup target add x86_64-unknown-linux-gnu + +WORKDIR /app + +# The command will be provided at runtime +CMD ["./build.sh"] diff --git a/build.sh b/build.sh index 0bbc5a5..2164aba 100755 --- a/build.sh +++ b/build.sh @@ -39,7 +39,6 @@ echo "--------------------" RUST_WIN_EXE="$RUST_DIR/target/x86_64-pc-windows-gnu/release/slang.exe" RUST_LINUX_BIN="$RUST_DIR/target/x86_64-unknown-linux-gnu/release/slang" CHARP_DLL="$CSHARP_DIR/bin/Release/net48/StationeersSlang.dll" -CHARP_PDB="$CSHARP_DIR/bin/Release/net48/StationeersSlang.pdb" # Check if the release dir exists, if not: create it. if [[ ! -d "$RELEASE_DIR" ]]; then @@ -49,4 +48,3 @@ fi cp "$RUST_WIN_EXE" "$RELEASE_DIR/slang.exe" cp "$RUST_LINUX_BIN" "$RELEASE_DIR/slang" cp "$CHARP_DLL" "$RELEASE_DIR/StationeersSlang.dll" -cp "$CHARP_PDB" "$RELEASE_DIR/StationeersSlang.pdb" diff --git a/csharp_mod/Extensions.cs b/csharp_mod/Extensions.cs index 35416fb..71499a6 100644 --- a/csharp_mod/Extensions.cs +++ b/csharp_mod/Extensions.cs @@ -3,6 +3,7 @@ namespace Slang; using System; using System.Collections.Generic; using System.Text; +using Assets.Scripts.UI; using StationeersIC10Editor; public static unsafe class SlangExtensions @@ -63,7 +64,9 @@ public static unsafe class SlangExtensions colIndex, token.length, color, - token.token_kind + token.token_kind, + 0, + token.tooltip.AsString() ); string errMsg = token.error.AsString(); @@ -115,20 +118,63 @@ public static unsafe class SlangExtensions { switch (kind) { - case 1: - return SlangFormatter.ColorString; // String - case 2: - return SlangFormatter.ColorString; // Number - case 3: - return SlangFormatter.ColorInstruction; // Boolean - case 4: - return SlangFormatter.ColorSelection; // Keyword - case 5: - return SlangFormatter.ColorLineNumber; // Identifier - case 6: - return SlangFormatter.ColorDefault; // Symbol + case 1: // Strings + return SlangFormatter.ColorString; + case 2: // Numbers + return SlangFormatter.ColorNumber; + case 3: // Booleans + return SlangFormatter.ColorBoolean; + + case 4: // (if, else, loop) + return SlangFormatter.ColorControl; + case 5: // (let, const, device) + return SlangFormatter.ColorDeclaration; + + case 6: // (variables) + return SlangFormatter.ColorIdentifier; + case 7: // (punctuation) + return SlangFormatter.ColorDefault; + + case 10: // (syscalls) + return SlangFormatter.ColorFunction; + + case 11: // Comparisons + case 12: // Math + case 13: // Logic + return SlangFormatter.ColorOperator; + default: return SlangFormatter.ColorDefault; } } + + public static unsafe List ToList(this Vec_FfiDocumentedItem_t vec) + { + var toReturn = new List((int)vec.len); + + var currentPtr = vec.ptr; + + for (int i = 0; i < (int)vec.len; i++) + { + var doc = currentPtr[i]; + var docItemName = doc.item_name.AsString(); + + var formattedText = TextMeshProFormatter.FromMarkdown(doc.docs.AsString()); + + var pediaPage = new StationpediaPage( + $"slang-item-{docItemName}", + docItemName, + formattedText + ); + + pediaPage.Text = formattedText; + pediaPage.Description = formattedText; + pediaPage.ParsePage(); + + toReturn.Add(pediaPage); + } + + Ffi.free_docs_vec(vec); + return toReturn; + } } diff --git a/csharp_mod/FfiGlue.cs b/csharp_mod/FfiGlue.cs index 5c49ef5..3489eb4 100644 --- a/csharp_mod/FfiGlue.cs +++ b/csharp_mod/FfiGlue.cs @@ -121,6 +121,31 @@ public unsafe partial class Ffi { slice_ref_uint16_t input); } +[StructLayout(LayoutKind.Sequential, Size = 48)] +public unsafe struct FfiDocumentedItem_t { + public Vec_uint8_t item_name; + + public Vec_uint8_t docs; +} + +/// +/// Same as [Vec][rust::Vec], but with guaranteed #[repr(C)] layout +/// +[StructLayout(LayoutKind.Sequential, Size = 24)] +public unsafe struct Vec_FfiDocumentedItem_t { + public FfiDocumentedItem_t * ptr; + + public UIntPtr len; + + public UIntPtr cap; +} + +public unsafe partial class Ffi { + [DllImport(RustLib, ExactSpelling = true)] public static unsafe extern + void free_docs_vec ( + Vec_FfiDocumentedItem_t v); +} + public unsafe partial class Ffi { [DllImport(RustLib, ExactSpelling = true)] public static unsafe extern void free_ffi_diagnostic_vec ( @@ -164,6 +189,11 @@ public unsafe partial class Ffi { Vec_uint8_t s); } +public unsafe partial class Ffi { + [DllImport(RustLib, ExactSpelling = true)] public static unsafe extern + Vec_FfiDocumentedItem_t get_docs (); +} + public unsafe partial class Ffi { [DllImport(RustLib, ExactSpelling = true)] public static unsafe extern Vec_FfiToken_t tokenize_line ( diff --git a/csharp_mod/Formatter.cs b/csharp_mod/Formatter.cs index a3dc844..13eaf16 100644 --- a/csharp_mod/Formatter.cs +++ b/csharp_mod/Formatter.cs @@ -4,26 +4,65 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; using System.Timers; +using Cysharp.Threading.Tasks; using StationeersIC10Editor; public class SlangFormatter : ICodeFormatter { private CancellationTokenSource? _lspCancellationToken; - private readonly SynchronizationContext? _mainThreadContext; - private volatile bool IsDiagnosing = false; + private object _tokenLock = new(); - public static readonly uint ColorInstruction = ColorFromHTML("#ffff00"); - public static readonly uint ColorString = ColorFromHTML("#ce9178"); + // VS Code Dark Theme Palette + public static readonly uint ColorControl = ColorFromHTML("#C586C0"); // Pink (if, return, loop) + public static readonly uint ColorDeclaration = ColorFromHTML("#569CD6"); // Blue (let, device, fn) + public static readonly uint ColorFunction = ColorFromHTML("#DCDCAA"); // Yellow (syscalls) + public static readonly uint ColorString = ColorFromHTML("#CE9178"); // Orange + public static new readonly uint ColorNumber = ColorFromHTML("#B5CEA8"); // Light Green + public static readonly uint ColorBoolean = ColorFromHTML("#569CD6"); // Blue (true/false) + public static readonly uint ColorIdentifier = ColorFromHTML("#9CDCFE"); // Light Blue (variables) + public static new readonly uint ColorDefault = ColorFromHTML("#D4D4D4"); // White (punctuation ; { } ) + + // Operators are often the same color as default text in VS Code Dark, + // but having a separate definition lets you tweak it (e.g. make them slightly darker or distinct) + public static readonly uint ColorOperator = ColorFromHTML("#D4D4D4"); private HashSet _linesWithErrors = new(); public SlangFormatter() + : base() { - // 1. Capture the Main Thread context. - // This works because the Editor instantiates this class on the main thread. - _mainThreadContext = SynchronizationContext.Current; + OnCodeChanged += HandleCodeChanged; + } + + public static double MatchingScore(string input) + { + // Empty input is not valid Slang + if (string.IsNullOrWhiteSpace(input)) + return 0d; + + // Run the compiler to get diagnostics + var diagnostics = Marshal.DiagnoseSource(input); + + // Count the number of actual Errors (Severity 1). + // We ignore Warnings (2), Info (3), etc. + double errorCount = diagnostics.Count(d => d.Severity == 1); + + // Get the total line count to calculate error density + double lineCount = input.Split('\n').Length; + + // Prevent division by zero + if (lineCount == 0) + return 0d; + + // Calculate score: Start at 1.0 (100%) and subtract the ratio of errors per line. + // Example: 10 lines with 0 errors = 1.0 + // Example: 10 lines with 2 errors = 0.8 + // Example: 10 lines with 10+ errors = 0.0 + double score = 1.0d - (errorCount / lineCount); + + // Clamp the result between 0 and 1 + return Math.Max(0d, Math.Min(1d, score)); } public override string Compile() @@ -33,65 +72,62 @@ public class SlangFormatter : ICodeFormatter public override Line ParseLine(string line) { - HandleCodeChanged(); return Marshal.TokenizeLine(line); } private void HandleCodeChanged() { - if (IsDiagnosing) - return; + CancellationToken token; + string inputSrc; + lock (_tokenLock) + { + _lspCancellationToken?.Cancel(); + _lspCancellationToken = new CancellationTokenSource(); + token = _lspCancellationToken.Token; + inputSrc = this.RawText; + } - _lspCancellationToken?.Cancel(); - _lspCancellationToken?.Dispose(); - - _lspCancellationToken = new CancellationTokenSource(); - - _ = Task.Run(() => HandleLsp(_lspCancellationToken.Token), _lspCancellationToken.Token); + HandleLsp(inputSrc, token).Forget(); } private void OnTimerElapsed(object sender, ElapsedEventArgs e) { } - private async Task HandleLsp(CancellationToken cancellationToken) + private async UniTaskVoid HandleLsp(string inputSrc, CancellationToken cancellationToken) { try { - await Task.Delay(200, cancellationToken); + await UniTask.SwitchToThreadPool(); if (cancellationToken.IsCancellationRequested) - { return; - } - // 3. Dispatch the UI update to the Main Thread - if (_mainThreadContext != null) - { - // Post ensures ApplyDiagnostics runs on the captured thread (Main Thread) - _mainThreadContext.Post(_ => ApplyDiagnostics(), null); - } - else - { - // Fallback: If context is null (rare in Unity), try running directly - // but warn, as this might crash if not thread-safe. - L.Warning("SynchronizationContext was null. Attempting direct update (risky)."); - ApplyDiagnostics(); - } + await System.Threading.Tasks.Task.Delay(200, cancellationToken: cancellationToken); + + if (cancellationToken.IsCancellationRequested) + return; + + var dict = Marshal + .DiagnoseSource(inputSrc) + .GroupBy(d => d.Range.StartLine) + .ToDictionary(g => g.Key); + + await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken); + + ApplyDiagnostics(dict); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + L.Error(ex.Message); } - finally { } } // This runs on the Main Thread - private void ApplyDiagnostics() + private void ApplyDiagnostics(Dictionary> dict) { - List diagnosis = Marshal.DiagnoseSource(this.RawText); - - var dict = diagnosis.GroupBy(d => d.Range.StartLine).ToDictionary(g => g.Key); - var linesToRefresh = new HashSet(dict.Keys); linesToRefresh.UnionWith(_linesWithErrors); - IsDiagnosing = true; - foreach (var lineIndex in linesToRefresh) { // safety check for out of bounds (in case lines were deleted) @@ -134,7 +170,5 @@ public class SlangFormatter : ICodeFormatter } _linesWithErrors = new HashSet(dict.Keys); - - IsDiagnosing = false; } } diff --git a/csharp_mod/Marshal.cs b/csharp_mod/Marshal.cs index c058e6b..3a7b385 100644 --- a/csharp_mod/Marshal.cs +++ b/csharp_mod/Marshal.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.InteropServices; +using Assets.Scripts.UI; using StationeersIC10Editor; public struct Range @@ -151,6 +152,14 @@ public static class Marshal } } + /// + /// Gets the currently documented items from the Slang compiler and returns new StationpediaPages with correct formatting. + /// + public static unsafe List GetSlangDocs() + { + return Ffi.get_docs().ToList(); + } + private static string ExtractNativeLibrary(string libName) { string destinationPath = Path.Combine(Path.GetTempPath(), libName); diff --git a/csharp_mod/Patches.cs b/csharp_mod/Patches.cs index 22a4dd0..a96809e 100644 --- a/csharp_mod/Patches.cs +++ b/csharp_mod/Patches.cs @@ -214,7 +214,6 @@ public static class SlangPatches [HarmonyPrefix] public static void isc_ButtonInputCancel() { - L.Info("ButtonInputCancel called on the InputSourceCode static instance."); if (_currentlyEditingMotherboard is null || _motherboardCachedCode is null) { return; @@ -225,4 +224,14 @@ public static class SlangPatches _currentlyEditingMotherboard = null; _motherboardCachedCode = null; } + + [HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))] + [HarmonyPostfix] + public static void Stationpedia_Regenerate() + { + foreach (var page in Marshal.GetSlangDocs()) + { + Stationpedia.Register(page); + } + } } diff --git a/csharp_mod/Plugin.cs b/csharp_mod/Plugin.cs index d4a8740..94d177b 100644 --- a/csharp_mod/Plugin.cs +++ b/csharp_mod/Plugin.cs @@ -35,14 +35,15 @@ namespace Slang } } - [BepInPlugin(PluginGuid, PluginName, "0.1.0")] + [BepInPlugin(PluginGuid, PluginName, PluginVersion)] [BepInDependency(StationeersIC10Editor.IC10EditorPlugin.PluginGuid)] public class SlangPlugin : BaseUnityPlugin { public const string PluginGuid = "com.biddydev.slang"; public const string PluginName = "Slang"; + public const string PluginVersion = "0.1.0"; - public static Mod MOD = new Mod(PluginName, "0.1.0"); + public static Mod MOD = new Mod(PluginName, PluginVersion); private Harmony? _harmony; diff --git a/csharp_mod/StationpediaDocumentation.cs b/csharp_mod/StationpediaDocumentation.cs deleted file mode 100644 index e03814a..0000000 --- a/csharp_mod/StationpediaDocumentation.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Assets.Scripts.UI; - -namespace Slang -{ - public static class SlangDocs - { - public static StationpediaPage[] Pages - { - get - { - return - [ - new StationpediaPage( - "slang-init", - "Slang", - "Slang is a new high level language built specifically for Stationeers" - ), - ]; - } - } - } -} diff --git a/csharp_mod/TmpFormatter.cs b/csharp_mod/TmpFormatter.cs new file mode 100644 index 0000000..aab5802 --- /dev/null +++ b/csharp_mod/TmpFormatter.cs @@ -0,0 +1,54 @@ +using System.Text.RegularExpressions; + +namespace Slang; + +public static class TextMeshProFormatter +{ + private const string CODE_COLOR = "#FFD700"; + + public static string FromMarkdown(string markdown) + { + if (string.IsNullOrEmpty(markdown)) + return ""; + + // 1. Normalize Line Endings + string text = markdown.Replace("\r\n", "\n"); + + // 2. Handle Code Blocks (```) + text = Regex.Replace( + text, + @"```\s*(.*?)\s*```", + match => + { + var codeContent = match.Groups[1].Value; + return $"{codeContent}"; // Gold color for code + }, + RegexOptions.Singleline + ); + + // 3. Handle Headers (## Header) + // Convert ## Header to large bold text + text = Regex.Replace( + text, + @"^##(\s+)?(.+)$", + "$1", + RegexOptions.Multiline + ); + + // 4. Handle Inline Code (`code`) + text = Regex.Replace(text, @"`([^`]+)`", $"$1"); + + // 5. Handle Bold (**text**) + text = Regex.Replace(text, @"\*\*(.+?)\*\*", "$1"); + + // 6. Handle Italics (*text*) + text = Regex.Replace(text, @"\*(.+?)\*", "$1"); + + // 7. Convert Newlines to TMP Line Breaks + // Stationpedia needs
or explicit newlines. + // Often just ensuring \n is preserved is enough, but
is safer for HTML-like parsers. + text = text.Replace("\n", "
"); + + return text; + } +} diff --git a/csharp_mod/stationeersSlang.csproj b/csharp_mod/stationeersSlang.csproj index d613b2e..0880395 100644 --- a/csharp_mod/stationeersSlang.csproj +++ b/csharp_mod/stationeersSlang.csproj @@ -11,34 +11,36 @@ - $(STATIONEERS_DIR)/rocketstation_Data/Managed - $(STATIONEERS_DIR)/BepInEx/core - $(ManagedDir)/netstandard.dll + ./ref/netstandard.dll False - $(BepInExDir)/BepInEx.dll + ./ref/BepInEx.dll False - $(BepInExDir)/0Harmony.dll + ./ref/0Harmony.dll False - $(ManagedDir)/UnityEngine.dll + ./ref/UnityEngine.dll False - $(ManagedDir)/UnityEngine.CoreModule.dll + ./ref/UnityEngine.CoreModule.dll False - $(ManagedDir)/Assembly-CSharp.dll + ./ref/Assembly-CSharp.dll + False + + + ./ref/UniTask.dll False @@ -47,11 +49,11 @@ False - $(STATIONEERS_DIR)/BepInEx/plugins/StationeersLaunchPad/LaunchPadBooster.dll + ./ref/LaunchPadBooster.dll False - $(STATIONEERS_DIR)/BepInEx/plugins/StationeersLaunchPad/StationeersMods.Interface.dll + ./ref/StationeersMods.Interface.dll False diff --git a/rust_compiler/Cargo.lock b/rust_compiler/Cargo.lock index b42d3c4..f12b00b 100644 --- a/rust_compiler/Cargo.lock +++ b/rust_compiler/Cargo.lock @@ -360,6 +360,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "helpers" +version = "0.1.0" + [[package]] name = "indexmap" version = "2.12.1" @@ -412,9 +416,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "lsp-types" @@ -495,7 +499,9 @@ name = "parser" version = "0.1.0" dependencies = [ "anyhow", + "helpers", "lsp-types", + "pretty_assertions", "quick-error", "tokenizer", ] @@ -828,6 +834,7 @@ dependencies = [ "anyhow", "clap", "compiler", + "helpers", "lsp-types", "parser", "quick-error", @@ -925,6 +932,7 @@ name = "tokenizer" version = "0.1.0" dependencies = [ "anyhow", + "helpers", "lsp-types", "quick-error", "rust_decimal", @@ -989,9 +997,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/rust_compiler/Cargo.toml b/rust_compiler/Cargo.toml index 8286678..c0b0744 100644 --- a/rust_compiler/Cargo.toml +++ b/rust_compiler/Cargo.toml @@ -40,6 +40,7 @@ rust_decimal = { workspace = true } tokenizer = { path = "libs/tokenizer" } parser = { path = "libs/parser" } compiler = { path = "libs/compiler" } +helpers = { path = "libs/helpers" } safer-ffi = { workspace = true } [dev-dependencies] diff --git a/rust_compiler/libs/compiler/src/test/branching.rs b/rust_compiler/libs/compiler/src/test/branching.rs index d23d880..1a852da 100644 --- a/rust_compiler/libs/compiler/src/test/branching.rs +++ b/rust_compiler/libs/compiler/src/test/branching.rs @@ -149,6 +149,7 @@ fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> { sub r0 sp 1 put db r0 99 #h L1: + sub sp sp 1 " } ); diff --git a/rust_compiler/libs/compiler/src/test/declaration_function_invocation.rs b/rust_compiler/libs/compiler/src/test/declaration_function_invocation.rs index 2e0c3c2..43e3131 100644 --- a/rust_compiler/libs/compiler/src/test/declaration_function_invocation.rs +++ b/rust_compiler/libs/compiler/src/test/declaration_function_invocation.rs @@ -34,6 +34,8 @@ fn no_arguments() -> anyhow::Result<()> { #[test] fn let_var_args() -> anyhow::Result<()> { + // !IMPORTANT this needs to be stabilized as it currently incorrectly calculates sp offset at + // both ends of the cleanup lifecycle let compiled = compile! { debug " @@ -64,6 +66,7 @@ fn let_var_args() -> anyhow::Result<()> { get r8 db r0 sub sp sp 1 move r9 r15 #i + sub sp sp 1 " } ); @@ -123,6 +126,7 @@ fn inline_literal_args() -> anyhow::Result<()> { get r8 db r0 sub sp sp 1 move r9 r15 #returnedValue + sub sp sp 1 " } ); @@ -164,6 +168,7 @@ fn mixed_args() -> anyhow::Result<()> { get r8 db r0 sub sp sp 1 move r9 r15 #returnValue + sub sp sp 1 " } ); diff --git a/rust_compiler/libs/compiler/src/test/declaration_literal.rs b/rust_compiler/libs/compiler/src/test/declaration_literal.rs index 9316e54..c42624c 100644 --- a/rust_compiler/libs/compiler/src/test/declaration_literal.rs +++ b/rust_compiler/libs/compiler/src/test/declaration_literal.rs @@ -56,6 +56,7 @@ fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()> push 7 #h push 8 #i push 9 #j + sub sp sp 3 " } ); diff --git a/rust_compiler/libs/compiler/src/v1.rs b/rust_compiler/libs/compiler/src/v1.rs index e4ef716..6633554 100644 --- a/rust_compiler/libs/compiler/src/v1.rs +++ b/rust_compiler/libs/compiler/src/v1.rs @@ -4,9 +4,10 @@ use parser::{ Parser as ASTParser, sys_call::{SysCall, System}, tree_node::{ - AssignmentExpression, BinaryExpression, BlockExpression, DeviceDeclarationExpression, - Expression, FunctionExpression, IfExpression, InvocationExpression, Literal, - LiteralOrVariable, LogicalExpression, LoopExpression, Span, Spanned, WhileExpression, + AssignmentExpression, BinaryExpression, BlockExpression, ConstDeclarationExpression, + DeviceDeclarationExpression, Expression, FunctionExpression, IfExpression, + InvocationExpression, Literal, LiteralOrVariable, LogicalExpression, LoopExpression, + MemberAccessExpression, Span, Spanned, WhileExpression, }, }; use quick_error::quick_error; @@ -33,6 +34,20 @@ macro_rules! debug { }; } +fn extract_literal(literal: Literal, allow_strings: bool) -> Result { + if !allow_strings && matches!(literal, Literal::String(_)) { + return Err(Error::Unknown( + "Literal strings are not allowed in this context".to_string(), + None, + )); + } + Ok(match literal { + Literal::String(s) => s, + Literal::Number(n) => n.to_string(), + Literal::Boolean(b) => if b { "1" } else { "0" }.into(), + }) +} + quick_error! { #[derive(Debug)] pub enum Error { @@ -57,6 +72,9 @@ quick_error! { AgrumentMismatch(func_name: String, span: Span) { display("Incorrect number of arguments passed into `{func_name}`") } + ConstAssignment(ident: String, span: Span) { + display("Attempted to re-assign a value to const variable `{ident}`") + } Unknown(reason: String, span: Option) { display("{reason}") } @@ -83,6 +101,7 @@ impl From for lsp_types::Diagnostic { DuplicateIdentifier(_, span) | UnknownIdentifier(_, span) | InvalidDevice(_, span) + | ConstAssignment(_, span) | AgrumentMismatch(_, span) => Diagnostic { range: span.into(), message: value.to_string(), @@ -266,6 +285,10 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { // decl_expr is Box> self.expression_declaration(var_name, *decl_expr, scope) } + Expression::ConstDeclaration(const_decl_expr) => { + self.expression_const_declaration(const_decl_expr.node, scope)?; + Ok(None) + } Expression::Assignment(assign_expr) => { self.expression_assignment(assign_expr.node, scope)?; Ok(None) @@ -332,6 +355,42 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { } } } + Expression::MemberAccess(access) => { + // "load" behavior (e.g. `let x = d0.On`) + let MemberAccessExpression { object, member } = access.node; + + // 1. Resolve the object to a device string (e.g., "d0" or "rX") + let (device_str, cleanup) = self.resolve_device(*object, scope)?; + + // 2. Allocate a temp register for the result + let result_name = self.next_temp_name(); + let loc = scope.add_variable(&result_name, LocationRequest::Temp)?; + let reg = self.resolve_register(&loc)?; + + // 3. Emit load instruction: l rX device member + self.write_output(format!("l {} {} {}", reg, device_str, member.node))?; + + // 4. Cleanup + if let Some(c) = cleanup { + scope.free_temp(c)?; + } + + Ok(Some(CompilationResult { + location: loc, + temp_name: Some(result_name), + })) + } + Expression::MethodCall(call) => { + // Methods are not yet fully supported (e.g. `d0.SomeFunc()`). + // This would likely map to specialized syscalls or batch instructions. + Err(Error::Unknown( + format!( + "Method calls are not yet supported: {}", + call.node.method.node + ), + Some(call.span), + )) + } Expression::Priority(inner_expr) => self.expression(*inner_expr, scope), Expression::Negation(inner_expr) => { // Compile negation as 0 - inner @@ -361,6 +420,24 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { } } + /// Resolves an expression to a device identifier string for use in instructions like `s` or `l`. + /// Returns (device_string, optional_cleanup_temp_name). + fn resolve_device<'v>( + &mut self, + expr: Spanned, + scope: &mut VariableScope<'v>, + ) -> Result<(String, Option), Error> { + // If it's a direct variable reference, check if it's a known device alias first + if let Expression::Variable(ref name) = expr.node + && let Some(device_id) = self.devices.get(&name.node) + { + return Ok((device_id.clone(), None)); + } + + // Otherwise, compile it as an operand (e.g. it might be a register holding a device hash/id) + self.compile_operand(expr, scope) + } + fn emit_variable_assignment( &mut self, var_name: &str, @@ -380,6 +457,14 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { VariableLocation::Stack(_) => { self.write_output(format!("push {}{debug_tag}", source_value.into()))?; } + VariableLocation::Constant(_) => { + return Err(Error::Unknown( + r#"Attempted to emit a variable assignent for a constant value. + This is a Compiler bug and should be reported to the developer."# + .into(), + None, + )); + } } Ok(()) @@ -526,6 +611,7 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { ))?; format!("r{}", VariableScope::TEMP_STACK_REGISTER) } + VariableLocation::Constant(_) => unreachable!(), }; self.emit_variable_assignment(&name_str, &var_loc, src_str)?; (var_loc, None) @@ -540,6 +626,35 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { scope, ); } + Expression::MemberAccess(access) => { + // Compile the member access (load instruction) + let result = self.expression( + Spanned { + node: Expression::MemberAccess(access), + span: name_span, // Use declaration span roughly + }, + scope, + )?; + + // Result is in a temp register + let Some(comp_res) = result else { + return Err(Error::Unknown( + "Member access did not return a value".into(), + Some(name_span), + )); + }; + + let var_loc = scope.add_variable(&name_str, LocationRequest::Persist)?; + let result_reg = self.resolve_register(&comp_res.location)?; + + self.emit_variable_assignment(&name_str, &var_loc, result_reg)?; + + if let Some(temp) = comp_res.temp_name { + scope.free_temp(temp)?; + } + + (var_loc, None) + } _ => { return Err(Error::Unknown( format!("`{name_str}` declaration of this type is not supported/implemented."), @@ -554,55 +669,101 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { })) } + fn expression_const_declaration<'v>( + &mut self, + expr: ConstDeclarationExpression, + scope: &mut VariableScope<'v>, + ) -> Result { + let ConstDeclarationExpression { + name: const_name, + value: const_value, + } = expr; + + Ok(CompilationResult { + location: scope.define_const(const_name.node, const_value.node)?, + temp_name: None, + }) + } + fn expression_assignment<'v>( &mut self, expr: AssignmentExpression, scope: &mut VariableScope<'v>, ) -> Result<(), Error> { let AssignmentExpression { - identifier, + assignee, expression, } = expr; - let location = match scope.get_location_of(&identifier.node) { - Ok(l) => l, - Err(_) => { - self.errors.push(Error::UnknownIdentifier( - identifier.node.clone(), - identifier.span, + match assignee.node { + Expression::Variable(identifier) => { + let location = match scope.get_location_of(&identifier.node) { + Ok(l) => l, + Err(_) => { + self.errors.push(Error::UnknownIdentifier( + identifier.node.clone(), + identifier.span, + )); + VariableLocation::Temporary(0) + } + }; + + let (val_str, cleanup) = self.compile_operand(*expression, scope)?; + + let debug_tag = if self.config.debug { + format!(" #{}", identifier.node) + } else { + String::new() + }; + + match location { + VariableLocation::Temporary(reg) | VariableLocation::Persistant(reg) => { + self.write_output(format!("move r{reg} {val_str}{debug_tag}"))?; + } + VariableLocation::Stack(offset) => { + // Calculate address: sp - offset + self.write_output(format!( + "sub r{0} sp {offset}", + VariableScope::TEMP_STACK_REGISTER + ))?; + // Store value to stack/db at address + self.write_output(format!( + "put db r{0} {val_str}{debug_tag}", + VariableScope::TEMP_STACK_REGISTER + ))?; + } + VariableLocation::Constant(_) => { + return Err(Error::ConstAssignment(identifier.node, identifier.span)); + } + } + + if let Some(name) = cleanup { + scope.free_temp(name)?; + } + } + Expression::MemberAccess(access) => { + // Set instruction: s device member value + let MemberAccessExpression { object, member } = access.node; + + let (device_str, dev_cleanup) = self.resolve_device(*object, scope)?; + let (val_str, val_cleanup) = self.compile_operand(*expression, scope)?; + + self.write_output(format!("s {} {} {}", device_str, member.node, val_str))?; + + if let Some(c) = dev_cleanup { + scope.free_temp(c)?; + } + if let Some(c) = val_cleanup { + scope.free_temp(c)?; + } + } + _ => { + return Err(Error::Unknown( + "Invalid assignment target. Only variables and member access are supported." + .into(), + Some(assignee.span), )); - VariableLocation::Temporary(0) } - }; - - let (val_str, cleanup) = self.compile_operand(*expression, scope)?; - - let debug_tag = if self.config.debug { - format!(" #{}", identifier.node) - } else { - String::new() - }; - - match location { - VariableLocation::Temporary(reg) | VariableLocation::Persistant(reg) => { - self.write_output(format!("move r{reg} {val_str}{debug_tag}"))?; - } - VariableLocation::Stack(offset) => { - // Calculate address: sp - offset - self.write_output(format!( - "sub r{0} sp {offset}", - VariableScope::TEMP_STACK_REGISTER - ))?; - // Store value to stack/db at address - self.write_output(format!( - "put db r{0} {val_str}{debug_tag}", - VariableScope::TEMP_STACK_REGISTER - ))?; - } - } - - if let Some(name) = cleanup { - scope.free_temp(name)?; } Ok(()) @@ -671,6 +832,9 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { VariableLocation::Persistant(reg) | VariableLocation::Temporary(reg) => { self.write_output(format!("push r{reg}"))?; } + VariableLocation::Constant(lit) => { + self.write_output(format!("push {}", extract_literal(lit, false)?))?; + } VariableLocation::Stack(stack_offset) => { self.write_output(format!( "sub r{0} sp {stack_offset}", @@ -705,6 +869,31 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { stack.free_temp(name)?; } } + Expression::MemberAccess(access) => { + // Compile member access to temp and push + let result_opt = self.expression( + Spanned { + node: Expression::MemberAccess(access), + span: Span { + start_col: 0, + end_col: 0, + start_line: 0, + end_line: 0, + }, // Dummy span + }, + stack, + )?; + + if let Some(result) = result_opt { + let reg_str = self.resolve_register(&result.location)?; + self.write_output(format!("push {reg_str}"))?; + if let Some(name) = result.temp_name { + stack.free_temp(name)?; + } + } else { + self.write_output("push 0")?; // Should fail ideally + } + } _ => { return Err(Error::Unknown( format!( @@ -905,6 +1094,10 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { fn resolve_register(&self, loc: &VariableLocation) -> Result { match loc { VariableLocation::Temporary(r) | VariableLocation::Persistant(r) => Ok(format!("r{r}")), + VariableLocation::Constant(_) => Err(Error::Unknown( + "Cannot resolve a constant value to register".into(), + None, + )), VariableLocation::Stack(_) => Err(Error::Unknown( "Cannot resolve Stack location directly to register string without context".into(), None, @@ -954,6 +1147,11 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { VariableLocation::Temporary(r) | VariableLocation::Persistant(r) => { Ok((format!("r{r}"), result.temp_name)) } + VariableLocation::Constant(lit) => match lit { + Literal::Number(n) => Ok((n.to_string(), None)), + Literal::Boolean(b) => Ok((if b { "1" } else { "0" }.to_string(), None)), + Literal::String(s) => Ok((s, None)), + }, VariableLocation::Stack(offset) => { // If it's on the stack, we must load it into a temp to use it as an operand let temp_name = self.next_temp_name(); @@ -1116,25 +1314,34 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { fn expression_block<'v>( &mut self, mut expr: BlockExpression, - scope: &mut VariableScope<'v>, + parent_scope: &mut VariableScope<'v>, ) -> Result<(), Error> { // First, sort the expressions to ensure functions are hoisted expr.0.sort_by(|a, b| { - if matches!(b.node, Expression::Function(_)) - && matches!(a.node, Expression::Function(_)) - { + if matches!( + b.node, + Expression::Function(_) | Expression::ConstDeclaration(_) + ) && matches!( + a.node, + Expression::Function(_) | Expression::ConstDeclaration(_) + ) { std::cmp::Ordering::Equal - } else if matches!(a.node, Expression::Function(_)) { + } else if matches!( + a.node, + Expression::Function(_) | Expression::ConstDeclaration(_) + ) { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater } }); + let mut scope = VariableScope::scoped(parent_scope); + for expr in expr.0 { if !self.declared_main && !matches!(expr.node, Expression::Function(_)) - && !scope.has_parent() + && !parent_scope.has_parent() { self.write_output("main:")?; self.declared_main = true; @@ -1142,11 +1349,11 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { match expr.node { Expression::Return(ret_expr) => { - self.expression_return(*ret_expr, scope)?; + self.expression_return(*ret_expr, &mut scope)?; } _ => { // Swallow errors within expressions so block can continue - if let Err(e) = self.expression(expr, scope).and_then(|result| { + if let Err(e) = self.expression(expr, &mut scope).and_then(|result| { // If the expression was a statement that returned a temp result (e.g. `1 + 2;` line), // we must free it to avoid leaking registers. if let Some(comp_res) = result @@ -1162,6 +1369,10 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { } } + if scope.stack_offset() > 0 { + self.write_output(format!("sub sp sp {}", scope.stack_offset()))?; + } + Ok(()) } @@ -1190,6 +1401,14 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { debug!(self, "#returnValue") ))?; } + VariableLocation::Constant(lit) => { + let str = extract_literal(lit, false)?; + self.write_output(format!( + "move r{} {str} {}", + VariableScope::RETURN_REGISTER, + debug!(self, "#returnValue") + ))? + } VariableLocation::Stack(offset) => { self.write_output(format!( "sub r{} sp {offset}", @@ -1252,6 +1471,23 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { scope.free_temp(name)?; } } + Expression::MemberAccess(access) => { + // Return result of member access + let res_opt = self.expression( + Spanned { + node: Expression::MemberAccess(access), + span: expr.span, + }, + scope, + )?; + if let Some(res) = res_opt { + let reg = self.resolve_register(&res.location)?; + self.write_output(format!("move r{} {}", VariableScope::RETURN_REGISTER, reg))?; + if let Some(temp) = res.temp_name { + scope.free_temp(temp)?; + } + } + } _ => { return Err(Error::Unknown( format!("Unsupported `return` statement: {:?}", expr), diff --git a/rust_compiler/libs/compiler/src/variable_manager.rs b/rust_compiler/libs/compiler/src/variable_manager.rs index ca47f1c..61dde65 100644 --- a/rust_compiler/libs/compiler/src/variable_manager.rs +++ b/rust_compiler/libs/compiler/src/variable_manager.rs @@ -3,6 +3,7 @@ // r1 - r7 : Temporary Variables // r8 - r14 : Persistant Variables +use parser::tree_node::Literal; use quick_error::quick_error; use std::collections::{HashMap, VecDeque}; @@ -43,6 +44,8 @@ pub enum VariableLocation { Persistant(u8), /// Represents a a stack offset (current stack - offset = variable loc) Stack(u16), + /// Represents a constant value and should be directly substituted as such. + Constant(Literal), } pub struct VariableScope<'a> { @@ -91,6 +94,8 @@ impl<'a> VariableScope<'a> { pub fn scoped(parent: &'a VariableScope<'a>) -> Self { Self { parent: Option::Some(parent), + temporary_vars: parent.temporary_vars.clone(), + persistant_vars: parent.persistant_vars.clone(), ..Default::default() } } @@ -140,24 +145,48 @@ impl<'a> VariableScope<'a> { Ok(var_location) } - pub fn get_location_of( + pub fn define_const( &mut self, var_name: impl Into, + value: Literal, ) -> Result { let var_name = var_name.into(); - let var = self - .var_lookup_table - .get(var_name.as_str()) - .cloned() - .ok_or(Error::UnknownVariable(var_name))?; - - if let VariableLocation::Stack(inserted_at_offset) = var { - Ok(VariableLocation::Stack( - self.stack_offset - inserted_at_offset, - )) - } else { - Ok(var) + if self.var_lookup_table.contains_key(&var_name) { + return Err(Error::DuplicateVariable(var_name)); } + + let new_value = VariableLocation::Constant(value); + + self.var_lookup_table.insert(var_name, new_value.clone()); + Ok(new_value) + } + + pub fn get_location_of(&self, var_name: impl Into) -> Result { + let var_name = var_name.into(); + + // 1. Check this scope + if let Some(var) = self.var_lookup_table.get(var_name.as_str()) { + if let VariableLocation::Stack(inserted_at_offset) = var { + // Return offset relative to CURRENT sp + return Ok(VariableLocation::Stack( + self.stack_offset - inserted_at_offset, + )); + } else { + return Ok(var.clone()); + } + } + + // 2. Recursively check parent + if let Some(parent) = self.parent { + let loc = parent.get_location_of(var_name)?; + + if let VariableLocation::Stack(parent_offset) = loc { + return Ok(VariableLocation::Stack(parent_offset + self.stack_offset)); + } + return Ok(loc); + } + + Err(Error::UnknownVariable(var_name)) } pub fn has_parent(&self) -> bool { @@ -180,7 +209,7 @@ impl<'a> VariableScope<'a> { "Attempted to free a `let` variable.", ))); } - VariableLocation::Stack(_) => {} + _ => {} }; Ok(()) diff --git a/rust_compiler/libs/helpers/Cargo.toml b/rust_compiler/libs/helpers/Cargo.toml new file mode 100644 index 0000000..cb9df2c --- /dev/null +++ b/rust_compiler/libs/helpers/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "helpers" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/rust_compiler/libs/helpers/src/lib.rs b/rust_compiler/libs/helpers/src/lib.rs new file mode 100644 index 0000000..40b9b5a --- /dev/null +++ b/rust_compiler/libs/helpers/src/lib.rs @@ -0,0 +1,15 @@ +mod macros; +mod syscall; + +/// This trait will allow the LSP to emit documentation for various tokens and expressions. +/// You can easily create documentation for large enums with the `documented!` macro. +pub trait Documentation { + /// Retreive documentation for this specific item. + fn docs(&self) -> String; + + fn get_all_documentation() -> Vec<(&'static str, String)>; +} + +pub mod prelude { + pub use super::{Documentation, documented, with_syscalls}; +} diff --git a/rust_compiler/libs/helpers/src/macros.rs b/rust_compiler/libs/helpers/src/macros.rs new file mode 100644 index 0000000..9c51e46 --- /dev/null +++ b/rust_compiler/libs/helpers/src/macros.rs @@ -0,0 +1,111 @@ +#[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 Documentation 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() + } + )* + } + } + + // 3. Implement Static Documentation Provider + #[allow(dead_code)] + fn get_all_documentation() -> Vec<(&'static str, String)> { + vec![ + $( + ( + stringify!($variant), + { + // Re-use the same extraction logic + let doc_lines: &[Option<&str>] = &[ + $( + documented!(@doc_filter #[ $($variant_attr)* ]) + ),* + ]; + doc_lines.iter() + .filter_map(|&d| d) + .collect::>() + .join("\n") + .trim() + .to_string() + } + ) + ),* + ] + } + } + }; +} + diff --git a/rust_compiler/libs/helpers/src/syscall.rs b/rust_compiler/libs/helpers/src/syscall.rs new file mode 100644 index 0000000..37daaa9 --- /dev/null +++ b/rust_compiler/libs/helpers/src/syscall.rs @@ -0,0 +1,32 @@ +#[macro_export] +macro_rules! with_syscalls { + ($matcher:ident) => { + $matcher!( + "yield", + "sleep", + "hash", + "loadFromDevice", + "loadBatchNamed", + "loadBatch", + "setOnDevice", + "setOnDeviceBatched", + "setOnDeviceBatchedNamed", + "acos", + "asin", + "atan", + "atan2", + "abs", + "ceil", + "cos", + "floor", + "log", + "max", + "min", + "rand", + "sin", + "sqrt", + "tan", + "trunc" + ); + }; +} diff --git a/rust_compiler/libs/parser/Cargo.toml b/rust_compiler/libs/parser/Cargo.toml index 5ff0cd5..336b498 100644 --- a/rust_compiler/libs/parser/Cargo.toml +++ b/rust_compiler/libs/parser/Cargo.toml @@ -6,8 +6,10 @@ edition = "2024" [dependencies] quick-error = { workspace = true } tokenizer = { path = "../tokenizer" } +helpers = { path = "../helpers" } 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 bf5217e..dd9f582 100644 --- a/rust_compiler/libs/parser/src/lib.rs +++ b/rust_compiler/libs/parser/src/lib.rs @@ -14,6 +14,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 { @@ -121,7 +125,6 @@ impl<'a> Parser<'a> { } /// Calculates a Span from a given Token reference. - /// This is a static helper to avoid borrowing `self` when we already have a token ref. fn token_to_span(t: &Token) -> Span { let len = t.original_string.as_ref().map(|s| s.len()).unwrap_or(0); Span { @@ -149,7 +152,6 @@ impl<'a> Parser<'a> { where F: FnOnce(&mut Self) -> Result, { - // Peek at the start token. If no current token (parsing hasn't started), peek the buffer. let start_token = if self.current_token.is_some() { self.current_token.clone() } else { @@ -163,7 +165,6 @@ impl<'a> Parser<'a> { let node = parser(self)?; - // The end token is the current_token after parsing. let end_token = self.current_token.as_ref(); let (end_line, end_col) = end_token @@ -184,26 +185,15 @@ impl<'a> Parser<'a> { }) } - /// Skips tokens until a statement boundary is found to recover from errors. fn synchronize(&mut self) -> Result<(), Error> { - // We advance once to consume the error-causing token if we haven't already - // But often the error happens after we consumed something. - // Safe bet: consume current, then look. - - // If we assign next, we might be skipping the very token we want to sync on if the error didn't consume it? - // Usually, in recursive descent, the error is raised when `current` is unexpected. - // We want to discard `current` and move on. self.assign_next()?; while let Some(token) = &self.current_token { if token.token_type == TokenType::Symbol(Symbol::Semicolon) { - // Consuming the semicolon is a good place to stop and resume parsing next statement self.assign_next()?; return Ok(()); } - // Check if the token looks like the start of a statement. - // If so, we don't consume it; we return so the loop in parse_all can try to parse it. match token.token_type { TokenType::Keyword(Keyword::Fn) | TokenType::Keyword(Keyword::Let) @@ -231,7 +221,6 @@ impl<'a> Parser<'a> { let mut expressions = Vec::>::new(); loop { - // Check EOF without unwrapping error match self.tokenizer.peek() { Ok(None) => break, Err(e) => { @@ -248,19 +237,13 @@ impl<'a> Parser<'a> { Ok(None) => break, Err(e) => { self.errors.push(e); - // Recover if self.synchronize().is_err() { - // If sync failed (e.g. EOF during sync), break break; } } } } - // Even if we had errors, we return whatever partial AST we managed to build. - // If expressions is empty and we had errors, it's a failed parse, but we return a block. - - // Use the last token position for end span, or start if nothing parsed let end_token_opt = self.tokenizer.peek().unwrap_or(None); let (end_line, end_col) = end_token_opt .map(|tok| { @@ -285,7 +268,6 @@ impl<'a> Parser<'a> { pub fn parse(&mut self) -> Result>, Error> { self.assign_next()?; - // If assign_next hit EOF or error? if self.current_token.is_none() { return Ok(None); } @@ -317,15 +299,18 @@ impl<'a> Parser<'a> { return Ok(None); }; - // check if the next or current token is an operator, comparison, or logical symbol + // Handle Postfix operators (Member Access, Method Call) immediately after unary + let lhs = self.parse_postfix(lhs)?; + + // Handle Infix operators (Binary, Logical, Assignment) if self_matches_peek!( self, - TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() + TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign) ) { return Ok(Some(self.infix(lhs)?)); } else if self_matches_current!( self, - TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() + TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign) ) { self.tokenizer.seek(SeekFrom::Current(-1))?; return Ok(Some(self.infix(lhs)?)); @@ -334,6 +319,116 @@ impl<'a> Parser<'a> { Ok(Some(lhs)) } + /// Handles dot notation chains: x.y.z() + fn parse_postfix( + &mut self, + mut lhs: Spanned, + ) -> Result, Error> { + loop { + if self_matches_peek!(self, TokenType::Symbol(Symbol::Dot)) { + self.assign_next()?; // consume Dot + + let identifier_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?; + let identifier_span = Self::token_to_span(identifier_token); + let identifier = match identifier_token.token_type { + TokenType::Identifier(ref id) => id.clone(), + _ => { + return Err(Error::UnexpectedToken( + identifier_span, + identifier_token.clone(), + )); + } + }; + + // Check for Method Call '()' + if self_matches_peek!(self, TokenType::Symbol(Symbol::LParen)) { + // Method Call + self.assign_next()?; // consume '(' + let mut arguments = Vec::>::new(); + + while !token_matches!( + self.get_next()?.ok_or(Error::UnexpectedEOF)?, + TokenType::Symbol(Symbol::RParen) + ) { + let expression = self.expression()?.ok_or(Error::UnexpectedEOF)?; + + // Block expressions not allowed in args + if let Expression::Block(_) = expression.node { + return Err(Error::InvalidSyntax( + self.current_span(), + String::from("Block expressions are not allowed in method calls"), + )); + } + arguments.push(expression); + + if !self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) + && !self_matches_peek!(self, TokenType::Symbol(Symbol::RParen)) + { + let next_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?; + return Err(Error::UnexpectedToken( + Self::token_to_span(next_token), + next_token.clone(), + )); + } + + if !self_matches_peek!(self, TokenType::Symbol(Symbol::RParen)) { + self.assign_next()?; + } + } + + // End span is the ')' + let end_span = self.current_span(); + let combined_span = Span { + start_line: lhs.span.start_line, + start_col: lhs.span.start_col, + end_line: end_span.end_line, + end_col: end_span.end_col, + }; + + lhs = Spanned { + span: combined_span, + node: Expression::MethodCall(Spanned { + span: combined_span, + node: MethodCallExpression { + object: boxed!(lhs), + method: Spanned { + span: identifier_span, + node: identifier, + }, + arguments, + }, + }), + }; + } else { + // Member Access + let combined_span = Span { + start_line: lhs.span.start_line, + start_col: lhs.span.start_col, + end_line: identifier_span.end_line, + end_col: identifier_span.end_col, + }; + + lhs = Spanned { + span: combined_span, + node: Expression::MemberAccess(Spanned { + span: combined_span, + node: MemberAccessExpression { + object: boxed!(lhs), + member: Spanned { + span: identifier_span, + node: identifier, + }, + }, + }), + }; + } + } else { + break; + } + } + Ok(lhs) + } + fn unary(&mut self) -> Result>, Error> { macro_rules! matches_keyword { ($keyword:expr, $($pattern:pat),+) => { @@ -357,10 +452,7 @@ impl<'a> Parser<'a> { )); } - TokenType::Keyword(Keyword::Let) => { - // declaration is wrapped in spanned inside the function, but expects 'let' to be current - Some(self.spanned(|p| p.declaration())?) - } + TokenType::Keyword(Keyword::Let) => Some(self.spanned(|p| p.declaration())?), TokenType::Keyword(Keyword::Device) => { let spanned_dev = self.spanned(|p| p.device())?; @@ -370,6 +462,15 @@ impl<'a> Parser<'a> { }) } + TokenType::Keyword(Keyword::Const) => { + let spanned_const = self.spanned(|p| p.const_declaration())?; + + Some(Spanned { + span: spanned_const.span, + node: Expression::ConstDeclaration(spanned_const), + }) + } + TokenType::Keyword(Keyword::Fn) => { let spanned_fn = self.spanned(|p| p.function())?; Some(Spanned { @@ -404,7 +505,6 @@ impl<'a> Parser<'a> { TokenType::Keyword(Keyword::Break) => { let span = self.current_span(); - // make sure the next token is a semi-colon let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?; if !token_matches!(next, TokenType::Symbol(Symbol::Semicolon)) { return Err(Error::UnexpectedToken( @@ -451,16 +551,6 @@ impl<'a> Parser<'a> { }) } - TokenType::Identifier(_) - if self_matches_peek!(self, TokenType::Symbol(Symbol::Assign)) => - { - let spanned_assign = self.spanned(|p| p.assignment())?; - Some(Spanned { - span: spanned_assign.span, - node: Expression::Assignment(spanned_assign), - }) - } - TokenType::Identifier(ref id) => { let span = self.current_span(); Some(Spanned { @@ -489,24 +579,36 @@ impl<'a> Parser<'a> { } TokenType::Symbol(Symbol::LParen) => { - // Priority handles its own spanning self.spanned(|p| p.priority())?.node.map(|node| *node) } TokenType::Symbol(Symbol::Minus) => { - // Need to handle span manually because unary call is next let start_span = self.current_span(); self.assign_next()?; let inner_expr = self.unary()?.ok_or(Error::UnexpectedEOF)?; + // NOTE: Unary negation can also have postfix applied to the inner expression + // But generally -a.b parses as -(a.b), which is what parse_postfix ensures if called here. + // However, we call parse_postfix on the RESULT of unary in expression(), so + // `expression` sees `Negation`. `parse_postfix` doesn't apply to Negation node unless we allow it? + // Actually, `x.y` binds tighter than `-`. `postfix` logic belongs inside `unary` logic or + // `expression` logic. + // If I have `-x.y`, standard precedence says `-(x.y)`. + // `unary` returns `Negation(x)`. Then `expression` calls `postfix` on `Negation(x)`. + // `postfix` loop runs on `Negation`. This implies `(-x).y`. This is usually WRONG. + // `.` binds tighter than `-`. + // So `unary` must call `postfix` on the *operand* of the negation. + + let inner_with_postfix = self.parse_postfix(inner_expr)?; + let combined_span = Span { start_line: start_span.start_line, start_col: start_span.start_col, - end_line: inner_expr.span.end_line, - end_col: inner_expr.span.end_col, + end_line: inner_with_postfix.span.end_line, + end_col: inner_with_postfix.span.end_col, }; Some(Spanned { span: combined_span, - node: Expression::Negation(boxed!(inner_expr)), + node: Expression::Negation(boxed!(inner_with_postfix)), }) } @@ -514,17 +616,18 @@ impl<'a> Parser<'a> { let start_span = self.current_span(); self.assign_next()?; let inner_expr = self.unary()?.ok_or(Error::UnexpectedEOF)?; + let inner_with_postfix = self.parse_postfix(inner_expr)?; let combined_span = Span { start_line: start_span.start_line, start_col: start_span.start_col, - end_line: inner_expr.span.end_line, - end_col: inner_expr.span.end_col, + end_line: inner_with_postfix.span.end_line, + end_col: inner_with_postfix.span.end_col, }; Some(Spanned { span: combined_span, node: Expression::Logical(Spanned { span: combined_span, - node: LogicalExpression::Not(boxed!(inner_expr)), + node: LogicalExpression::Not(boxed!(inner_with_postfix)), }), }) } @@ -543,41 +646,44 @@ impl<'a> Parser<'a> { fn get_infix_child_node(&mut self) -> Result, Error> { let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?; - match current_token.token_type { + let start_span = self.current_span(); + + let expr = match current_token.token_type { TokenType::Number(_) | TokenType::Boolean(_) => { let lit = self.spanned(|p| p.literal())?; - Ok(Spanned { + Spanned { span: lit.span, node: Expression::Literal(lit), - }) + } } TokenType::Identifier(ref ident) if !self_matches_peek!(self, TokenType::Symbol(Symbol::LParen)) => { + // This is a Variable. We need to check for Postfix operations on it. let span = self.current_span(); - Ok(Spanned { + Spanned { span, node: Expression::Variable(Spanned { span, node: ident.clone(), }), - }) + } } - TokenType::Symbol(Symbol::LParen) => Ok(*self + TokenType::Symbol(Symbol::LParen) => *self .spanned(|p| p.priority())? .node - .ok_or(Error::UnexpectedEOF)?), + .ok_or(Error::UnexpectedEOF)?, + TokenType::Identifier(_) if self_matches_peek!(self, TokenType::Symbol(Symbol::LParen)) => { let inv = self.spanned(|p| p.invocation())?; - Ok(Spanned { + Spanned { span: inv.span, node: Expression::Invocation(inv), - }) + } } TokenType::Symbol(Symbol::Minus) => { - let start_span = self.current_span(); self.assign_next()?; let inner = self.get_infix_child_node()?; let span = Span { @@ -586,13 +692,12 @@ impl<'a> Parser<'a> { end_line: inner.span.end_line, end_col: inner.span.end_col, }; - Ok(Spanned { + Spanned { span, node: Expression::Negation(boxed!(inner)), - }) + } } TokenType::Symbol(Symbol::LogicalNot) => { - let start_span = self.current_span(); self.assign_next()?; let inner = self.get_infix_child_node()?; let span = Span { @@ -601,19 +706,25 @@ impl<'a> Parser<'a> { end_line: inner.span.end_line, end_col: inner.span.end_col, }; - Ok(Spanned { + Spanned { span, node: Expression::Logical(Spanned { span, node: LogicalExpression::Not(boxed!(inner)), }), - }) + } } - _ => Err(Error::UnexpectedToken( - self.current_span(), - current_token.clone(), - )), - } + _ => { + return Err(Error::UnexpectedToken( + self.current_span(), + current_token.clone(), + )); + } + }; + + // Important: We must check for postfix operations here too + // e.g. a + b.c + self.parse_postfix(expr) } fn device(&mut self) -> Result { @@ -665,39 +776,6 @@ impl<'a> Parser<'a> { }) } - fn assignment(&mut self) -> Result { - let identifier_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?; - let identifier_span = Self::token_to_span(identifier_token); - let identifier = match identifier_token.token_type { - TokenType::Identifier(ref id) => id.clone(), - _ => { - return Err(Error::UnexpectedToken( - self.current_span(), - self.current_token.clone().ok_or(Error::UnexpectedEOF)?, - )); - } - }; - - let current_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?.clone(); - if !token_matches!(current_token, TokenType::Symbol(Symbol::Assign)) { - return Err(Error::UnexpectedToken( - Self::token_to_span(¤t_token), - current_token.clone(), - )); - } - self.assign_next()?; - - let expression = self.expression()?.ok_or(Error::UnexpectedEOF)?; - - Ok(AssignmentExpression { - identifier: Spanned { - span: identifier_span, - node: identifier, - }, - expression: boxed!(expression), - }) - } - fn infix(&mut self, previous: Spanned) -> Result, Error> { let current_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?.clone(); @@ -708,7 +786,9 @@ impl<'a> Parser<'a> { | Expression::Priority(_) | Expression::Literal(_) | Expression::Variable(_) - | Expression::Negation(_) => {} + | Expression::Negation(_) + | Expression::MemberAccess(_) + | Expression::MethodCall(_) => {} _ => { return Err(Error::InvalidSyntax( self.current_span(), @@ -722,9 +802,10 @@ impl<'a> Parser<'a> { let mut temp_token = current_token.clone(); + // Include Assign in the operator loop while token_matches!( temp_token, - TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() + TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign) ) { let operator = match temp_token.token_type { TokenType::Symbol(s) => s, @@ -955,6 +1036,37 @@ impl<'a> Parser<'a> { } operators.retain(|symbol| !matches!(symbol, Symbol::LogicalOr)); + // --- PRECEDENCE LEVEL 8: Assignment (=) --- + // Assignment is Right Associative: a = b = c => a = (b = c) + // We iterate Right to Left + for (i, operator) in operators.iter().enumerate().rev() { + if matches!(operator, Symbol::Assign) { + let right = expressions.remove(i + 1); + let left = expressions.remove(i); + let span = Span { + start_line: left.span.start_line, + start_col: left.span.start_col, + end_line: right.span.end_line, + end_col: right.span.end_col, + }; + + expressions.insert( + i, + Spanned { + span, + node: Expression::Assignment(Spanned { + span, + node: AssignmentExpression { + assignee: boxed!(left), + expression: boxed!(right), + }, + }), + }, + ); + } + } + operators.retain(|symbol| !matches!(symbol, Symbol::Assign)); + if expressions.len() != 1 || !operators.is_empty() { return Err(Error::InvalidSyntax( self.current_span(), @@ -1117,6 +1229,46 @@ impl<'a> Parser<'a> { Ok(BlockExpression(expressions)) } + fn const_declaration(&mut self) -> Result { + // const + let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?; + if !self_matches_current!(self, TokenType::Keyword(Keyword::Const)) { + return Err(Error::UnexpectedToken( + self.current_span(), + current_token.clone(), + )); + } + + // variable_name + let ident_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?; + let ident_span = Self::token_to_span(ident_token); + let ident = match ident_token.token_type { + TokenType::Identifier(ref id) => id.clone(), + _ => return Err(Error::UnexpectedToken(ident_span, ident_token.clone())), + }; + + // `=` + let assign_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?.clone(); + if !token_matches!(assign_token, TokenType::Symbol(Symbol::Assign)) { + return Err(Error::UnexpectedToken( + Self::token_to_span(&assign_token), + assign_token, + )); + } + + // literal value + self.assign_next()?; + let lit = self.spanned(|p| p.literal())?; + + Ok(ConstDeclarationExpression { + name: Spanned { + span: ident_span, + node: ident, + }, + value: lit, + }) + } + fn declaration(&mut self) -> Result { let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?; if !self_matches_current!(self, TokenType::Keyword(Keyword::Let)) { @@ -1506,7 +1658,6 @@ impl<'a> Parser<'a> { Literal::String(variable), ))) } - // ... (implementing other syscalls similarly using patterns above) "setOnDevice" => { check_length(self, &invocation.arguments, 3)?; let mut args = invocation.arguments.into_iter(); @@ -1531,23 +1682,10 @@ impl<'a> Parser<'a> { boxed!(variable), ))) } - _ => { - // For Math functions or unknown functions - if SysCall::is_syscall(&invocation.name.node) { - // Attempt to parse as math if applicable, or error if strict - // Here we are falling back to simple handling or error. - // Since Math isn't fully expanded in this snippet, we return Unsupported. - Err(Error::UnsupportedKeyword( - self.current_span(), - self.current_token.clone().ok_or(Error::UnexpectedEOF)?, - )) - } else { - Err(Error::UnsupportedKeyword( - self.current_span(), - self.current_token.clone().ok_or(Error::UnexpectedEOF)?, - )) - } - } + _ => Err(Error::UnsupportedKeyword( + self.current_span(), + self.current_token.clone().ok_or(Error::UnexpectedEOF)?, + )), } } } diff --git a/rust_compiler/libs/parser/src/sys_call.rs b/rust_compiler/libs/parser/src/sys_call.rs index 494fac5..6a4aa88 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 helpers::prelude::*; -#[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. + /// ## IC10 + /// `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,76 @@ 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. + /// ## IC10 + /// `yield` + /// ## Slang + /// `yield();` + Yield, + /// Represents a function that can be called to sleep for a certain amount of time. + /// ## IC10 + /// `sleep a(r?|num)` + /// ## Slang + /// `sleep(number|var);` + Sleep(Box>), + /// Gets the in-game hash for a specific prefab name. + /// ## IC10 + /// `HASH("prefabName")` + /// ## Slang + /// `HASH("prefabName");` + Hash(Literal), + /// Represents a function which loads a device variable into a register. + /// ## IC10 + /// `l r? d? var` + /// ## Slang + /// `loadFromDevice(deviceType, "LogicType");` + 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 + /// ## IC10 + /// `lbn r? deviceHash nameHash logicType batchMode` + /// ## Slang + /// `loadFromDeviceBatchedNamed(deviceHash, deviceName, "LogicType", "BatchMode");` + LoadBatchNamed( + LiteralOrVariable, + Box>, + Literal, + Literal, + ), + /// Loads a LogicType from all connected network devices, aggregating them via a + /// batchMode + /// ## IC10 + /// `lb r? deviceHash logicType batchMode` + /// ## Slang + /// `loadFromDeviceBatched(deviceHash, "Variable", "LogicType");` + LoadBatch(LiteralOrVariable, Literal, Literal), + /// Represents a function which stores a setting into a specific device. + /// ## IC10 + /// `s d? logicType r?` + /// ## Slang + /// `setOnDevice(deviceType, "Variable", (number|var));` + SetOnDevice(LiteralOrVariable, Literal, Box>), + /// Represents a function which stores a setting to all devices that match + /// the given deviceHash + /// ## IC10 + /// `sb deviceHash logicType r?` + SetOnDeviceBatched(LiteralOrVariable, Literal, Box>), + /// Represents a function which stores a setting to all devices that match + /// both the given deviceHash AND the given nameHash + /// ## IC10 + /// `sbn deviceHash nameHash logicType r?` + /// ## Slang + /// `setOnDeviceBatchedNamed(deviceType, nameHash, "LogicType", (number|var))` + SetOnDeviceBatchedNamed( + LiteralOrVariable, + LiteralOrVariable, + Literal, + Box>, + ), + } } impl std::fmt::Display for System { @@ -190,6 +229,22 @@ pub enum SysCall { Math(Math), } +impl Documentation for SysCall { + fn docs(&self) -> String { + match self { + Self::System(s) => s.docs(), + Self::Math(m) => m.docs(), + } + } + + fn get_all_documentation() -> Vec<(&'static str, String)> { + let mut all_docs = System::get_all_documentation(); + all_docs.extend(Math::get_all_documentation()); + + all_docs + } +} + impl std::fmt::Display for SysCall { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -201,32 +256,6 @@ impl std::fmt::Display for SysCall { impl SysCall { pub fn is_syscall(identifier: &str) -> bool { - matches!( - identifier, - "yield" - | "sleep" - | "hash" - | "loadFromDevice" - | "setOnDevice" - | "setOnDeviceBatched" - | "setOnDeviceBatchedNamed" - | "acos" - | "asin" - | "atan" - | "atan2" - | "abs" - | "ceil" - | "cos" - | "floor" - | "log" - | "max" - | "min" - | "rand" - | "sin" - | "sqrt" - | "tan" - | "trunc" - ) + tokenizer::token::is_syscall(identifier) } } - diff --git a/rust_compiler/libs/parser/src/test/mod.rs b/rust_compiler/libs/parser/src/test/mod.rs index c23869a..beb874c 100644 --- a/rust_compiler/libs/parser/src/test/mod.rs +++ b/rust_compiler/libs/parser/src/test/mod.rs @@ -1,7 +1,7 @@ #[macro_export] macro_rules! parser { ($input:expr) => { - Parser::new(Tokenizer::from($input.to_owned())) + Parser::new(Tokenizer::from($input)) }; } @@ -9,6 +9,7 @@ mod blocks; use super::Parser; use super::Tokenizer; use anyhow::Result; +use pretty_assertions::assert_eq; #[test] fn test_unsupported_keywords() -> Result<()> { @@ -31,7 +32,7 @@ fn test_declarations() -> Result<()> { // The below line should fail let y = 234 "#; - let tokenizer = Tokenizer::from(input.to_owned()); + let tokenizer = Tokenizer::from(input); let mut parser = Parser::new(tokenizer); let expression = parser.parse()?.unwrap(); @@ -43,6 +44,36 @@ fn test_declarations() -> Result<()> { Ok(()) } +#[test] +fn test_const_declaration() -> Result<()> { + let input = r#" + const item = 20c; + const decimal = 200.15; + const nameConst = "str_lit"; + "#; + let tokenizer = Tokenizer::from(input); + let mut parser = Parser::new(tokenizer); + + assert_eq!( + "(const item = 293.15)", + parser.parse()?.unwrap().to_string() + ); + + assert_eq!( + "(const decimal = 200.15)", + parser.parse()?.unwrap().to_string() + ); + + assert_eq!( + r#"(const nameConst = "str_lit")"#, + parser.parse()?.unwrap().to_string() + ); + + assert_eq!(None, parser.parse()?); + + Ok(()) +} + #[test] fn test_function_expression() -> Result<()> { let input = r#" @@ -52,7 +83,7 @@ fn test_function_expression() -> Result<()> { } "#; - let tokenizer = Tokenizer::from(input.to_owned()); + let tokenizer = Tokenizer::from(input); let mut parser = Parser::new(tokenizer); let expression = parser.parse()?.unwrap(); @@ -71,7 +102,7 @@ fn test_function_invocation() -> Result<()> { add(); "#; - let tokenizer = Tokenizer::from(input.to_owned()); + let tokenizer = Tokenizer::from(input); let mut parser = Parser::new(tokenizer); let expression = parser.parse()?.unwrap(); @@ -87,7 +118,7 @@ fn test_priority_expression() -> Result<()> { let x = (4); "#; - let tokenizer = Tokenizer::from(input.to_owned()); + let tokenizer = Tokenizer::from(input); let mut parser = Parser::new(tokenizer); let expression = parser.parse()?.unwrap(); @@ -99,16 +130,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(()) diff --git a/rust_compiler/libs/parser/src/tree_node.rs b/rust_compiler/libs/parser/src/tree_node.rs index a968ed4..daca733 100644 --- a/rust_compiler/libs/parser/src/tree_node.rs +++ b/rust_compiler/libs/parser/src/tree_node.rs @@ -74,13 +74,13 @@ impl std::fmt::Display for LogicalExpression { #[derive(Debug, PartialEq, Eq)] pub struct AssignmentExpression { - pub identifier: Spanned, + pub assignee: Box>, pub expression: Box>, } impl std::fmt::Display for AssignmentExpression { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "({} = {})", self.identifier, self.expression) + write!(f, "({} = {})", self.assignee, self.expression) } } @@ -145,6 +145,41 @@ impl std::fmt::Display for InvocationExpression { } } +#[derive(Debug, PartialEq, Eq)] +pub struct MemberAccessExpression { + pub object: Box>, + pub member: Spanned, +} + +impl std::fmt::Display for MemberAccessExpression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.object, self.member) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct MethodCallExpression { + pub object: Box>, + pub method: Spanned, + pub arguments: Vec>, +} + +impl std::fmt::Display for MethodCallExpression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}.{}({})", + self.object, + self.method, + self.arguments + .iter() + .map(|e| e.to_string()) + .collect::>() + .join(", ") + ) + } +} + #[derive(Debug, PartialEq, Eq)] pub enum LiteralOrVariable { Literal(Literal), @@ -160,6 +195,18 @@ impl std::fmt::Display for LiteralOrVariable { } } +#[derive(Debug, PartialEq, Eq)] +pub struct ConstDeclarationExpression { + pub name: Spanned, + pub value: Spanned, +} + +impl std::fmt::Display for ConstDeclarationExpression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "(const {} = {})", self.name, self.value) + } +} + #[derive(Debug, PartialEq, Eq)] pub struct DeviceDeclarationExpression { /// any variable-like name @@ -281,6 +328,7 @@ pub enum Expression { Binary(Spanned), Block(Spanned), Break(Span), + ConstDeclaration(Spanned), Continue(Span), Declaration(Spanned, Box>), DeviceDeclaration(Spanned), @@ -290,6 +338,8 @@ pub enum Expression { Literal(Spanned), Logical(Spanned), Loop(Spanned), + MemberAccess(Spanned), + MethodCall(Spanned), Negation(Box>), Priority(Box>), Return(Box>), @@ -305,6 +355,7 @@ impl std::fmt::Display for Expression { Expression::Binary(e) => write!(f, "{}", e), Expression::Block(e) => write!(f, "{}", e), Expression::Break(_) => write!(f, "break"), + Expression::ConstDeclaration(e) => write!(f, "{}", e), Expression::Continue(_) => write!(f, "continue"), Expression::Declaration(id, e) => write!(f, "(let {} = {})", id, e), Expression::DeviceDeclaration(e) => write!(f, "{}", e), @@ -314,6 +365,8 @@ impl std::fmt::Display for Expression { Expression::Literal(l) => write!(f, "{}", l), Expression::Logical(e) => write!(f, "{}", e), Expression::Loop(e) => write!(f, "{}", e), + Expression::MemberAccess(e) => write!(f, "{}", e), + Expression::MethodCall(e) => write!(f, "{}", e), Expression::Negation(e) => write!(f, "(-{})", e), Expression::Priority(e) => write!(f, "({})", e), Expression::Return(e) => write!(f, "(return {})", e), diff --git a/rust_compiler/libs/tokenizer/Cargo.toml b/rust_compiler/libs/tokenizer/Cargo.toml index 38032f4..7433cab 100644 --- a/rust_compiler/libs/tokenizer/Cargo.toml +++ b/rust_compiler/libs/tokenizer/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" rust_decimal = { workspace = true } quick-error = { workspace = true } lsp-types = { workspace = true } +helpers = { path = "../helpers" } [dev-dependencies] anyhow = { version = "^1" } diff --git a/rust_compiler/libs/tokenizer/src/lib.rs b/rust_compiler/libs/tokenizer/src/lib.rs index c6a5fba..b73c514 100644 --- a/rust_compiler/libs/tokenizer/src/lib.rs +++ b/rust_compiler/libs/tokenizer/src/lib.rs @@ -199,7 +199,7 @@ impl<'a> Tokenizer<'a> { .tokenize_symbol(next_char, start_line, start_col) .map(Some); } - char if char.is_alphabetic() => { + char if char.is_alphabetic() || char == '_' => { return self .tokenize_keyword_or_identifier(next_char, start_line, start_col) .map(Some); @@ -439,14 +439,15 @@ impl<'a> Tokenizer<'a> { }}; } macro_rules! next_ws { - () => { matches!(self.peek_next_char()?, Some(x) if x.is_whitespace() || !x.is_alphanumeric()) || self.peek_next_char()?.is_none() }; + () => { matches!(self.peek_next_char()?, Some(x) if x.is_whitespace() || (!x.is_alphanumeric()) && x != '_') || self.peek_next_char()?.is_none() }; } let mut buffer = String::with_capacity(16); let mut looped_char = Some(first_char); while let Some(next_char) = looped_char { - if next_char.is_whitespace() || !next_char.is_alphanumeric() { + // allow UNDERSCORE_IDENTS + if next_char.is_whitespace() || (!next_char.is_alphanumeric() && next_char != '_') { break; } buffer.push(next_char); @@ -463,6 +464,7 @@ impl<'a> Tokenizer<'a> { "break" if next_ws!() => keyword!(Break), "while" if next_ws!() => keyword!(While), "continue" if next_ws!() => keyword!(Continue), + "const" if next_ws!() => keyword!(Const), "true" if next_ws!() => { return Ok(Token::new( TokenType::Boolean(true), @@ -837,7 +839,9 @@ mod tests { #[test] fn test_keyword_parse() -> Result<()> { - let mut tokenizer = Tokenizer::from(String::from("let fn if else return enum")); + let mut tokenizer = Tokenizer::from(String::from( + "let fn if else return enum continue break const", + )); let expected_tokens = vec![ TokenType::Keyword(Keyword::Let), @@ -846,6 +850,9 @@ mod tests { TokenType::Keyword(Keyword::Else), TokenType::Keyword(Keyword::Return), TokenType::Keyword(Keyword::Enum), + TokenType::Keyword(Keyword::Continue), + TokenType::Keyword(Keyword::Break), + TokenType::Keyword(Keyword::Const), ]; for expected_token in expected_tokens { @@ -859,7 +866,7 @@ mod tests { #[test] fn test_identifier_parse() -> Result<()> { - let mut tokenizer = Tokenizer::from(String::from("fn test")); + let mut tokenizer = Tokenizer::from(String::from("fn test fn test_underscores")); let token = tokenizer.next_token()?.unwrap(); assert_eq!(token.token_type, TokenType::Keyword(Keyword::Fn)); @@ -868,6 +875,13 @@ mod tests { token.token_type, TokenType::Identifier(String::from("test")) ); + let token = tokenizer.next_token()?.unwrap(); + assert_eq!(token.token_type, TokenType::Keyword(Keyword::Fn)); + let token = tokenizer.next_token()?.unwrap(); + assert_eq!( + token.token_type, + TokenType::Identifier(String::from("test_underscores")) + ); Ok(()) } diff --git a/rust_compiler/libs/tokenizer/src/token.rs b/rust_compiler/libs/tokenizer/src/token.rs index b471d6d..3befb9f 100644 --- a/rust_compiler/libs/tokenizer/src/token.rs +++ b/rust_compiler/libs/tokenizer/src/token.rs @@ -1,5 +1,15 @@ +use helpers::prelude::*; use rust_decimal::Decimal; +// Define a local macro to consume the list +macro_rules! generate_check { + ($($name:literal),*) => { + pub fn is_syscall(s: &str) -> bool { + matches!(s, $($name)|*) + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub struct Token { /// The type of the token @@ -87,17 +97,56 @@ pub enum TokenType { EOF, } +impl Documentation for TokenType { + fn docs(&self) -> String { + match self { + Self::Keyword(k) => k.docs(), + _ => "".into(), + } + } + + fn get_all_documentation() -> Vec<(&'static str, String)> { + Keyword::get_all_documentation() + } +} + +helpers::with_syscalls!(generate_check); + impl From for u32 { fn from(value: TokenType) -> Self { - use TokenType::*; match value { - String(_) => 1, - Number(_) => 2, - Boolean(_) => 3, - Keyword(_) => 4, - Identifier(_) => 5, - Symbol(_) => 6, - EOF => 0, + TokenType::String(_) => 1, + TokenType::Number(_) => 2, + TokenType::Boolean(_) => 3, + TokenType::Keyword(k) => match k { + Keyword::If + | Keyword::Else + | Keyword::Loop + | Keyword::While + | Keyword::Break + | Keyword::Continue + | Keyword::Return => 4, + _ => 5, + }, + TokenType::Identifier(s) => { + if is_syscall(&s) { + 10 + } else { + 6 + } + } + TokenType::Symbol(s) => { + if s.is_comparison() { + 11 + } else if s.is_operator() { + 12 + } else if s.is_logical() { + 13 + } else { + 7 + } + } + TokenType::EOF => 0, } } } @@ -264,28 +313,147 @@ impl std::fmt::Display for Symbol { } } -#[derive(Debug, PartialEq, Hash, Eq, Clone, Copy)] -pub enum Keyword { - /// Represents the `continue` keyword - Continue, - /// Represents the `let` keyword - Let, - /// Represents the `fn` keyword - Fn, - /// Represents the `if` keyword - If, - /// Represents the `device` keyword. Useful for defining a device at a specific address (ex. d0, d1, d2, etc.) - Device, - /// Represents the `else` keyword - Else, - /// Represents the `return` keyword - Return, - /// Represents the `enum` keyword - Enum, - /// Represents the `loop` keyword - Loop, - /// Represents the `break` keyword - Break, - /// Represents the `while` keyword - While, +documented! { + #[derive(Debug, PartialEq, Hash, Eq, Clone, Copy)] + pub enum Keyword { + /// Represents the `continue` keyword. This will allow you to bypass the current iteration in a loop and start the next one. + /// ## Example + /// ``` + /// let item = 0; + /// loop { + /// if (item % 2 == 0) { + /// // This will NOT increment `item` and will continue with the next iteration of the + /// // loop + /// continue; + /// } + /// item = item + 1; + /// } + /// ``` + Continue, + /// Prepresents the `const` keyword. This allows you to define a variable that will never + /// change throughout the lifetime of the program, similar to `define` in IC10. If you are + /// not planning on mutating the variable (changing it), it is recommend you store it as a + /// const, as the compiler will not assign it to a register or stack variable. + /// + /// ## Example + /// ``` + /// const targetTemp = 20c; + /// device gasSensor = "d0"; + /// device airCon = "d1"; + /// + /// airCon.On = gasSensor.Temperature > targetTemp; + /// ``` + Const, + /// Represents the `let` keyword, used to declare variables within Slang. + /// ## Example + /// ``` + /// // This variable now exists either in a register or the stack depending on how many + /// // free registers were available when declaring it. + /// let item = 0; + /// ``` + Let, + /// Represents the `fn` keyword, used to declare functions within Slang. + /// # WARNING + /// Functions are currently unstable and are subject to change until stabilized. Use at + /// your own risk! (They are also heavily not optimized and produce a LOT of code bloat) + /// ## Example + /// ``` + /// // This allows you to now call `doSomething` with specific arguments. + /// fn doSomething(arg1, arg2) { + /// + /// } + /// ``` + Fn, + /// Represents the `if` keyword, allowing you to create branched logic. + /// ## Example + /// ``` + /// let i = 0; + /// if (i == 0) { + /// i = 1; + /// } + /// // At this line, `i` is now `1` + /// ``` + If, + /// Represents the `device` keyword. Useful for defining a device at a specific address + /// (ex. d0, d1, d2, etc.). This also allows you to perform direct operations ON a device. + /// ## Example + /// ``` + /// device self = "db"; + /// + /// // This is the same as `s db Setting 123` + /// self.Setting = 123; + /// ``` + Device, + /// Represents the `else` keyword. Useful if you want to check a condition but run run + /// seperate logic in case that condition fails. + /// ## Example + /// ``` + /// device self = "db"; + /// let i = 0; + /// if (i < 0) { + /// self.Setting = 0; + /// } else { + /// self.Setting = 1; + /// } + /// // Here, the `Setting` on the current housing is `1` because i was NOT less than 0 + /// ``` + Else, + /// Represents the `return` keyword. Allows you to pass values from a function back to + /// the caller. + /// ## Example + /// ``` + /// fn doSomething() { + /// return 1 + 2; + /// } + /// + /// // `returnedValue` now holds the value `3` + /// let returnedValue = doSomething(); + /// ``` + Return, + /// Represents the `enum` keyword. This is currently not supported, but is kept as a + /// reserved keyword in the future case that this is implemented. + Enum, + /// Represents the `loop` keyword. This allows you to create an infinate loop, but can be + /// broken with the `break` keyword. + /// ## Example + /// ``` + /// device self = "db"; + /// let i = 0; + /// loop { + /// i = i + 1; + /// // The current housing will infinately increment it's `Setting` value. + /// self.Setting = i; + /// } + /// ``` + Loop, + /// Represents the `break` keyword. This allows you to "break out of" a loop prematurely, + /// such as when an if() conditon is true, etc. + /// ## Example + /// ``` + /// let i = 0; + /// // This loop will run until the value of `i` is greater than 10,000, + /// // which will then trigger the `break` keyword and it will stop looping + /// loop { + /// if (i > 10_000) { + /// break; + /// } + /// i = i + 1; + /// } + /// ``` + Break, + /// Represents the `while` keyword. This is similar to the `loop` keyword but different in + /// that you don't need an `if` statement to break out of a loop, that is handled + /// automatically when invoking `while` + /// ## Example + /// ``` + /// let i = 0; + /// // This loop will run until the value of `i` is greater than 10,000, in which case the + /// // while loop will automatically stop running and code will continue AFTER the last + /// // bracket. + /// while (i < 10_000) { + /// i = i + 1; + /// } + /// ``` + While, + } } diff --git a/rust_compiler/src/ffi/mod.rs b/rust_compiler/src/ffi/mod.rs index 5c0750e..ee31887 100644 --- a/rust_compiler/src/ffi/mod.rs +++ b/rust_compiler/src/ffi/mod.rs @@ -1,5 +1,6 @@ use compiler::Compiler; -use parser::Parser; +use helpers::Documentation; +use parser::{sys_call::SysCall, Parser}; use safer_ffi::prelude::*; use std::io::BufWriter; use tokenizer::{ @@ -26,6 +27,13 @@ pub struct FfiRange { end_line: u32, } +#[derive_ReprC] +#[repr(C)] +pub struct FfiDocumentedItem { + item_name: safer_ffi::String, + docs: safer_ffi::String, +} + impl From for FfiRange { fn from(value: lsp_types::Range) -> Self { Self { @@ -76,6 +84,11 @@ pub fn free_string(s: safer_ffi::String) { drop(s) } +#[ffi_export] +pub fn free_docs_vec(v: safer_ffi::Vec) { + drop(v) +} + /// C# handles strings as UTF16. We do NOT want to allocate that memory in C# because /// we want to avoid GC. So we pass it to Rust to handle all the memory allocations. /// This should result in the ability to compile many times without triggering frame drops @@ -151,8 +164,8 @@ pub fn tokenize_line(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec) -> safer_ffi::Vec< res.unwrap_or(vec![].into()) } + +#[ffi_export] +pub fn get_docs() -> safer_ffi::Vec { + let res = std::panic::catch_unwind(|| { + let mut docs = SysCall::get_all_documentation(); + docs.extend(TokenType::get_all_documentation()); + + docs + }); + + let Ok(result) = res else { + return vec![].into(); + }; + + result + .into_iter() + .map(|(key, doc)| FfiDocumentedItem { + item_name: key.into(), + docs: doc.into(), + }) + .collect::>() + .into() +}