Merge pull request #11 from dbidwell94/dot-notation

Dot notation and bug improvements
This commit is contained in:
2025-12-05 02:49:35 -07:00
committed by GitHub
32 changed files with 1658 additions and 492 deletions

69
.github/workflows/build.yml vendored Normal file
View File

@@ -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/

19
Dockerfile.build Normal file
View File

@@ -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"]

View File

@@ -39,7 +39,6 @@ echo "--------------------"
RUST_WIN_EXE="$RUST_DIR/target/x86_64-pc-windows-gnu/release/slang.exe" 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" RUST_LINUX_BIN="$RUST_DIR/target/x86_64-unknown-linux-gnu/release/slang"
CHARP_DLL="$CSHARP_DIR/bin/Release/net48/StationeersSlang.dll" 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. # Check if the release dir exists, if not: create it.
if [[ ! -d "$RELEASE_DIR" ]]; then if [[ ! -d "$RELEASE_DIR" ]]; then
@@ -49,4 +48,3 @@ fi
cp "$RUST_WIN_EXE" "$RELEASE_DIR/slang.exe" cp "$RUST_WIN_EXE" "$RELEASE_DIR/slang.exe"
cp "$RUST_LINUX_BIN" "$RELEASE_DIR/slang" cp "$RUST_LINUX_BIN" "$RELEASE_DIR/slang"
cp "$CHARP_DLL" "$RELEASE_DIR/StationeersSlang.dll" cp "$CHARP_DLL" "$RELEASE_DIR/StationeersSlang.dll"
cp "$CHARP_PDB" "$RELEASE_DIR/StationeersSlang.pdb"

View File

@@ -3,6 +3,7 @@ namespace Slang;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Assets.Scripts.UI;
using StationeersIC10Editor; using StationeersIC10Editor;
public static unsafe class SlangExtensions public static unsafe class SlangExtensions
@@ -63,7 +64,9 @@ public static unsafe class SlangExtensions
colIndex, colIndex,
token.length, token.length,
color, color,
token.token_kind token.token_kind,
0,
token.tooltip.AsString()
); );
string errMsg = token.error.AsString(); string errMsg = token.error.AsString();
@@ -115,20 +118,63 @@ public static unsafe class SlangExtensions
{ {
switch (kind) switch (kind)
{ {
case 1: case 1: // Strings
return SlangFormatter.ColorString; // String return SlangFormatter.ColorString;
case 2: case 2: // Numbers
return SlangFormatter.ColorString; // Number return SlangFormatter.ColorNumber;
case 3: case 3: // Booleans
return SlangFormatter.ColorInstruction; // Boolean return SlangFormatter.ColorBoolean;
case 4:
return SlangFormatter.ColorSelection; // Keyword case 4: // (if, else, loop)
case 5: return SlangFormatter.ColorControl;
return SlangFormatter.ColorLineNumber; // Identifier case 5: // (let, const, device)
case 6: return SlangFormatter.ColorDeclaration;
return SlangFormatter.ColorDefault; // Symbol
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: default:
return SlangFormatter.ColorDefault; return SlangFormatter.ColorDefault;
} }
} }
public static unsafe List<StationpediaPage> ToList(this Vec_FfiDocumentedItem_t vec)
{
var toReturn = new List<StationpediaPage>((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;
}
} }

View File

@@ -121,6 +121,31 @@ public unsafe partial class Ffi {
slice_ref_uint16_t input); 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;
}
/// <summary>
/// Same as [<c>Vec<T></c>][<c>rust::Vec</c>], but with guaranteed <c>#[repr(C)]</c> layout
/// </summary>
[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 { public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern [DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
void free_ffi_diagnostic_vec ( void free_ffi_diagnostic_vec (
@@ -164,6 +189,11 @@ public unsafe partial class Ffi {
Vec_uint8_t s); 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 { public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern [DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
Vec_FfiToken_t tokenize_line ( Vec_FfiToken_t tokenize_line (

View File

@@ -4,26 +4,65 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Timers; using System.Timers;
using Cysharp.Threading.Tasks;
using StationeersIC10Editor; using StationeersIC10Editor;
public class SlangFormatter : ICodeFormatter public class SlangFormatter : ICodeFormatter
{ {
private CancellationTokenSource? _lspCancellationToken; private CancellationTokenSource? _lspCancellationToken;
private readonly SynchronizationContext? _mainThreadContext; private object _tokenLock = new();
private volatile bool IsDiagnosing = false;
public static readonly uint ColorInstruction = ColorFromHTML("#ffff00"); // VS Code Dark Theme Palette
public static readonly uint ColorString = ColorFromHTML("#ce9178"); 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<uint> _linesWithErrors = new(); private HashSet<uint> _linesWithErrors = new();
public SlangFormatter() public SlangFormatter()
: base()
{ {
// 1. Capture the Main Thread context. OnCodeChanged += HandleCodeChanged;
// This works because the Editor instantiates this class on the main thread. }
_mainThreadContext = SynchronizationContext.Current;
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() public override string Compile()
@@ -33,65 +72,62 @@ public class SlangFormatter : ICodeFormatter
public override Line ParseLine(string line) public override Line ParseLine(string line)
{ {
HandleCodeChanged();
return Marshal.TokenizeLine(line); return Marshal.TokenizeLine(line);
} }
private void HandleCodeChanged() private void HandleCodeChanged()
{ {
if (IsDiagnosing) CancellationToken token;
return; string inputSrc;
lock (_tokenLock)
{
_lspCancellationToken?.Cancel();
_lspCancellationToken = new CancellationTokenSource();
token = _lspCancellationToken.Token;
inputSrc = this.RawText;
}
_lspCancellationToken?.Cancel(); HandleLsp(inputSrc, token).Forget();
_lspCancellationToken?.Dispose();
_lspCancellationToken = new CancellationTokenSource();
_ = Task.Run(() => HandleLsp(_lspCancellationToken.Token), _lspCancellationToken.Token);
} }
private void OnTimerElapsed(object sender, ElapsedEventArgs e) { } private void OnTimerElapsed(object sender, ElapsedEventArgs e) { }
private async Task HandleLsp(CancellationToken cancellationToken) private async UniTaskVoid HandleLsp(string inputSrc, CancellationToken cancellationToken)
{ {
try try
{ {
await Task.Delay(200, cancellationToken); await UniTask.SwitchToThreadPool();
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{
return; return;
}
// 3. Dispatch the UI update to the Main Thread await System.Threading.Tasks.Task.Delay(200, cancellationToken: cancellationToken);
if (_mainThreadContext != null)
{ if (cancellationToken.IsCancellationRequested)
// Post ensures ApplyDiagnostics runs on the captured thread (Main Thread) return;
_mainThreadContext.Post(_ => ApplyDiagnostics(), null);
} var dict = Marshal
else .DiagnoseSource(inputSrc)
{ .GroupBy(d => d.Range.StartLine)
// Fallback: If context is null (rare in Unity), try running directly .ToDictionary(g => g.Key);
// but warn, as this might crash if not thread-safe.
L.Warning("SynchronizationContext was null. Attempting direct update (risky)."); await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
ApplyDiagnostics();
} ApplyDiagnostics(dict);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
L.Error(ex.Message);
} }
finally { }
} }
// This runs on the Main Thread // This runs on the Main Thread
private void ApplyDiagnostics() private void ApplyDiagnostics(Dictionary<uint, IGrouping<uint, Diagnostic>> dict)
{ {
List<Diagnostic> diagnosis = Marshal.DiagnoseSource(this.RawText);
var dict = diagnosis.GroupBy(d => d.Range.StartLine).ToDictionary(g => g.Key);
var linesToRefresh = new HashSet<uint>(dict.Keys); var linesToRefresh = new HashSet<uint>(dict.Keys);
linesToRefresh.UnionWith(_linesWithErrors); linesToRefresh.UnionWith(_linesWithErrors);
IsDiagnosing = true;
foreach (var lineIndex in linesToRefresh) foreach (var lineIndex in linesToRefresh)
{ {
// safety check for out of bounds (in case lines were deleted) // safety check for out of bounds (in case lines were deleted)
@@ -134,7 +170,5 @@ public class SlangFormatter : ICodeFormatter
} }
_linesWithErrors = new HashSet<uint>(dict.Keys); _linesWithErrors = new HashSet<uint>(dict.Keys);
IsDiagnosing = false;
} }
} }

View File

@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Assets.Scripts.UI;
using StationeersIC10Editor; using StationeersIC10Editor;
public struct Range public struct Range
@@ -151,6 +152,14 @@ public static class Marshal
} }
} }
/// <summary>
/// Gets the currently documented items from the Slang compiler and returns new StationpediaPages with correct formatting.
/// </summary>
public static unsafe List<StationpediaPage> GetSlangDocs()
{
return Ffi.get_docs().ToList();
}
private static string ExtractNativeLibrary(string libName) private static string ExtractNativeLibrary(string libName)
{ {
string destinationPath = Path.Combine(Path.GetTempPath(), libName); string destinationPath = Path.Combine(Path.GetTempPath(), libName);

View File

@@ -214,7 +214,6 @@ public static class SlangPatches
[HarmonyPrefix] [HarmonyPrefix]
public static void isc_ButtonInputCancel() public static void isc_ButtonInputCancel()
{ {
L.Info("ButtonInputCancel called on the InputSourceCode static instance.");
if (_currentlyEditingMotherboard is null || _motherboardCachedCode is null) if (_currentlyEditingMotherboard is null || _motherboardCachedCode is null)
{ {
return; return;
@@ -225,4 +224,14 @@ public static class SlangPatches
_currentlyEditingMotherboard = null; _currentlyEditingMotherboard = null;
_motherboardCachedCode = null; _motherboardCachedCode = null;
} }
[HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))]
[HarmonyPostfix]
public static void Stationpedia_Regenerate()
{
foreach (var page in Marshal.GetSlangDocs())
{
Stationpedia.Register(page);
}
}
} }

View File

@@ -35,14 +35,15 @@ namespace Slang
} }
} }
[BepInPlugin(PluginGuid, PluginName, "0.1.0")] [BepInPlugin(PluginGuid, PluginName, PluginVersion)]
[BepInDependency(StationeersIC10Editor.IC10EditorPlugin.PluginGuid)] [BepInDependency(StationeersIC10Editor.IC10EditorPlugin.PluginGuid)]
public class SlangPlugin : BaseUnityPlugin public class SlangPlugin : BaseUnityPlugin
{ {
public const string PluginGuid = "com.biddydev.slang"; public const string PluginGuid = "com.biddydev.slang";
public const string PluginName = "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; private Harmony? _harmony;

View File

@@ -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"
),
];
}
}
}
}

View File

@@ -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 $"<color={CODE_COLOR}>{codeContent}</color>"; // Gold color for code
},
RegexOptions.Singleline
);
// 3. Handle Headers (## Header)
// Convert ## Header to large bold text
text = Regex.Replace(
text,
@"^##(\s+)?(.+)$",
"<size=120%><b>$1</b></size>",
RegexOptions.Multiline
);
// 4. Handle Inline Code (`code`)
text = Regex.Replace(text, @"`([^`]+)`", $"<color={CODE_COLOR}>$1</color>");
// 5. Handle Bold (**text**)
text = Regex.Replace(text, @"\*\*(.+?)\*\*", "<b>$1</b>");
// 6. Handle Italics (*text*)
text = Regex.Replace(text, @"\*(.+?)\*", "<i>$1</i>");
// 7. Convert Newlines to TMP Line Breaks
// Stationpedia needs <br> or explicit newlines.
// Often just ensuring \n is preserved is enough, but <br> is safer for HTML-like parsers.
text = text.Replace("\n", "<br>");
return text;
}
}

View File

@@ -11,34 +11,36 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<ManagedDir>$(STATIONEERS_DIR)/rocketstation_Data/Managed</ManagedDir>
<BepInExDir>$(STATIONEERS_DIR)/BepInEx/core</BepInExDir>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="netstandard"> <Reference Include="netstandard">
<HintPath>$(ManagedDir)/netstandard.dll</HintPath> <HintPath>./ref/netstandard.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="BepInEx"> <Reference Include="BepInEx">
<HintPath>$(BepInExDir)/BepInEx.dll</HintPath> <HintPath>./ref/BepInEx.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="0Harmony"> <Reference Include="0Harmony">
<HintPath>$(BepInExDir)/0Harmony.dll</HintPath> <HintPath>./ref/0Harmony.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="UnityEngine"> <Reference Include="UnityEngine">
<HintPath>$(ManagedDir)/UnityEngine.dll</HintPath> <HintPath>./ref/UnityEngine.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="UnityEngine.CoreModule"> <Reference Include="UnityEngine.CoreModule">
<HintPath>$(ManagedDir)/UnityEngine.CoreModule.dll</HintPath> <HintPath>./ref/UnityEngine.CoreModule.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="Assembly-CSharp"> <Reference Include="Assembly-CSharp">
<HintPath>$(ManagedDir)/Assembly-CSharp.dll</HintPath> <HintPath>./ref/Assembly-CSharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UniTask">
<HintPath>./ref/UniTask.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
@@ -47,11 +49,11 @@
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="LaunchPadBooster.dll"> <Reference Include="LaunchPadBooster.dll">
<HintPath>$(STATIONEERS_DIR)/BepInEx/plugins/StationeersLaunchPad/LaunchPadBooster.dll</HintPath> <HintPath>./ref/LaunchPadBooster.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="StationeersMods.Interface.dll"> <Reference Include="StationeersMods.Interface.dll">
<HintPath>$(STATIONEERS_DIR)/BepInEx/plugins/StationeersLaunchPad/StationeersMods.Interface.dll</HintPath> <HintPath>./ref/StationeersMods.Interface.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
</ItemGroup> </ItemGroup>

View File

@@ -360,6 +360,10 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "helpers"
version = "0.1.0"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.12.1" version = "2.12.1"
@@ -412,9 +416,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.177" version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]] [[package]]
name = "lsp-types" name = "lsp-types"
@@ -495,7 +499,9 @@ name = "parser"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"helpers",
"lsp-types", "lsp-types",
"pretty_assertions",
"quick-error", "quick-error",
"tokenizer", "tokenizer",
] ]
@@ -828,6 +834,7 @@ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"compiler", "compiler",
"helpers",
"lsp-types", "lsp-types",
"parser", "parser",
"quick-error", "quick-error",
@@ -925,6 +932,7 @@ name = "tokenizer"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"helpers",
"lsp-types", "lsp-types",
"quick-error", "quick-error",
"rust_decimal", "rust_decimal",
@@ -989,9 +997,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.18.1" version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",

View File

@@ -40,6 +40,7 @@ rust_decimal = { workspace = true }
tokenizer = { path = "libs/tokenizer" } tokenizer = { path = "libs/tokenizer" }
parser = { path = "libs/parser" } parser = { path = "libs/parser" }
compiler = { path = "libs/compiler" } compiler = { path = "libs/compiler" }
helpers = { path = "libs/helpers" }
safer-ffi = { workspace = true } safer-ffi = { workspace = true }
[dev-dependencies] [dev-dependencies]

View File

@@ -149,6 +149,7 @@ fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> {
sub r0 sp 1 sub r0 sp 1
put db r0 99 #h put db r0 99 #h
L1: L1:
sub sp sp 1
" "
} }
); );

View File

@@ -34,6 +34,8 @@ fn no_arguments() -> anyhow::Result<()> {
#[test] #[test]
fn let_var_args() -> anyhow::Result<()> { 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! { let compiled = compile! {
debug debug
" "
@@ -64,6 +66,7 @@ fn let_var_args() -> anyhow::Result<()> {
get r8 db r0 get r8 db r0
sub sp sp 1 sub sp sp 1
move r9 r15 #i move r9 r15 #i
sub sp sp 1
" "
} }
); );
@@ -123,6 +126,7 @@ fn inline_literal_args() -> anyhow::Result<()> {
get r8 db r0 get r8 db r0
sub sp sp 1 sub sp sp 1
move r9 r15 #returnedValue move r9 r15 #returnedValue
sub sp sp 1
" "
} }
); );
@@ -164,6 +168,7 @@ fn mixed_args() -> anyhow::Result<()> {
get r8 db r0 get r8 db r0
sub sp sp 1 sub sp sp 1
move r9 r15 #returnValue move r9 r15 #returnValue
sub sp sp 1
" "
} }
); );

View File

@@ -56,6 +56,7 @@ fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()>
push 7 #h push 7 #h
push 8 #i push 8 #i
push 9 #j push 9 #j
sub sp sp 3
" "
} }
); );

View File

@@ -4,9 +4,10 @@ use parser::{
Parser as ASTParser, Parser as ASTParser,
sys_call::{SysCall, System}, sys_call::{SysCall, System},
tree_node::{ tree_node::{
AssignmentExpression, BinaryExpression, BlockExpression, DeviceDeclarationExpression, AssignmentExpression, BinaryExpression, BlockExpression, ConstDeclarationExpression,
Expression, FunctionExpression, IfExpression, InvocationExpression, Literal, DeviceDeclarationExpression, Expression, FunctionExpression, IfExpression,
LiteralOrVariable, LogicalExpression, LoopExpression, Span, Spanned, WhileExpression, InvocationExpression, Literal, LiteralOrVariable, LogicalExpression, LoopExpression,
MemberAccessExpression, Span, Spanned, WhileExpression,
}, },
}; };
use quick_error::quick_error; use quick_error::quick_error;
@@ -33,6 +34,20 @@ macro_rules! debug {
}; };
} }
fn extract_literal(literal: Literal, allow_strings: bool) -> Result<String, Error> {
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! { quick_error! {
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
@@ -57,6 +72,9 @@ quick_error! {
AgrumentMismatch(func_name: String, span: Span) { AgrumentMismatch(func_name: String, span: Span) {
display("Incorrect number of arguments passed into `{func_name}`") 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<Span>) { Unknown(reason: String, span: Option<Span>) {
display("{reason}") display("{reason}")
} }
@@ -83,6 +101,7 @@ impl From<Error> for lsp_types::Diagnostic {
DuplicateIdentifier(_, span) DuplicateIdentifier(_, span)
| UnknownIdentifier(_, span) | UnknownIdentifier(_, span)
| InvalidDevice(_, span) | InvalidDevice(_, span)
| ConstAssignment(_, span)
| AgrumentMismatch(_, span) => Diagnostic { | AgrumentMismatch(_, span) => Diagnostic {
range: span.into(), range: span.into(),
message: value.to_string(), message: value.to_string(),
@@ -266,6 +285,10 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
// decl_expr is Box<Spanned<Expression>> // decl_expr is Box<Spanned<Expression>>
self.expression_declaration(var_name, *decl_expr, scope) 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) => { Expression::Assignment(assign_expr) => {
self.expression_assignment(assign_expr.node, scope)?; self.expression_assignment(assign_expr.node, scope)?;
Ok(None) 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::Priority(inner_expr) => self.expression(*inner_expr, scope),
Expression::Negation(inner_expr) => { Expression::Negation(inner_expr) => {
// Compile negation as 0 - inner // 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<Expression>,
scope: &mut VariableScope<'v>,
) -> Result<(String, Option<String>), 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( fn emit_variable_assignment(
&mut self, &mut self,
var_name: &str, var_name: &str,
@@ -380,6 +457,14 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
VariableLocation::Stack(_) => { VariableLocation::Stack(_) => {
self.write_output(format!("push {}{debug_tag}", source_value.into()))?; 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(()) Ok(())
@@ -526,6 +611,7 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
))?; ))?;
format!("r{}", VariableScope::TEMP_STACK_REGISTER) format!("r{}", VariableScope::TEMP_STACK_REGISTER)
} }
VariableLocation::Constant(_) => unreachable!(),
}; };
self.emit_variable_assignment(&name_str, &var_loc, src_str)?; self.emit_variable_assignment(&name_str, &var_loc, src_str)?;
(var_loc, None) (var_loc, None)
@@ -540,6 +626,35 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
scope, 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( return Err(Error::Unknown(
format!("`{name_str}` declaration of this type is not supported/implemented."), 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<CompilationResult, Error> {
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>( fn expression_assignment<'v>(
&mut self, &mut self,
expr: AssignmentExpression, expr: AssignmentExpression,
scope: &mut VariableScope<'v>, scope: &mut VariableScope<'v>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let AssignmentExpression { let AssignmentExpression {
identifier, assignee,
expression, expression,
} = expr; } = expr;
let location = match scope.get_location_of(&identifier.node) { match assignee.node {
Ok(l) => l, Expression::Variable(identifier) => {
Err(_) => { let location = match scope.get_location_of(&identifier.node) {
self.errors.push(Error::UnknownIdentifier( Ok(l) => l,
identifier.node.clone(), Err(_) => {
identifier.span, 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(()) Ok(())
@@ -671,6 +832,9 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
VariableLocation::Persistant(reg) | VariableLocation::Temporary(reg) => { VariableLocation::Persistant(reg) | VariableLocation::Temporary(reg) => {
self.write_output(format!("push r{reg}"))?; self.write_output(format!("push r{reg}"))?;
} }
VariableLocation::Constant(lit) => {
self.write_output(format!("push {}", extract_literal(lit, false)?))?;
}
VariableLocation::Stack(stack_offset) => { VariableLocation::Stack(stack_offset) => {
self.write_output(format!( self.write_output(format!(
"sub r{0} sp {stack_offset}", "sub r{0} sp {stack_offset}",
@@ -705,6 +869,31 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
stack.free_temp(name)?; 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( return Err(Error::Unknown(
format!( format!(
@@ -905,6 +1094,10 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
fn resolve_register(&self, loc: &VariableLocation) -> Result<String, Error> { fn resolve_register(&self, loc: &VariableLocation) -> Result<String, Error> {
match loc { match loc {
VariableLocation::Temporary(r) | VariableLocation::Persistant(r) => Ok(format!("r{r}")), 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( VariableLocation::Stack(_) => Err(Error::Unknown(
"Cannot resolve Stack location directly to register string without context".into(), "Cannot resolve Stack location directly to register string without context".into(),
None, None,
@@ -954,6 +1147,11 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
VariableLocation::Temporary(r) | VariableLocation::Persistant(r) => { VariableLocation::Temporary(r) | VariableLocation::Persistant(r) => {
Ok((format!("r{r}"), result.temp_name)) 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) => { VariableLocation::Stack(offset) => {
// If it's on the stack, we must load it into a temp to use it as an operand // 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(); let temp_name = self.next_temp_name();
@@ -1116,25 +1314,34 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
fn expression_block<'v>( fn expression_block<'v>(
&mut self, &mut self,
mut expr: BlockExpression, mut expr: BlockExpression,
scope: &mut VariableScope<'v>, parent_scope: &mut VariableScope<'v>,
) -> Result<(), Error> { ) -> Result<(), Error> {
// First, sort the expressions to ensure functions are hoisted // First, sort the expressions to ensure functions are hoisted
expr.0.sort_by(|a, b| { expr.0.sort_by(|a, b| {
if matches!(b.node, Expression::Function(_)) if matches!(
&& matches!(a.node, Expression::Function(_)) b.node,
{ Expression::Function(_) | Expression::ConstDeclaration(_)
) && matches!(
a.node,
Expression::Function(_) | Expression::ConstDeclaration(_)
) {
std::cmp::Ordering::Equal std::cmp::Ordering::Equal
} else if matches!(a.node, Expression::Function(_)) { } else if matches!(
a.node,
Expression::Function(_) | Expression::ConstDeclaration(_)
) {
std::cmp::Ordering::Less std::cmp::Ordering::Less
} else { } else {
std::cmp::Ordering::Greater std::cmp::Ordering::Greater
} }
}); });
let mut scope = VariableScope::scoped(parent_scope);
for expr in expr.0 { for expr in expr.0 {
if !self.declared_main if !self.declared_main
&& !matches!(expr.node, Expression::Function(_)) && !matches!(expr.node, Expression::Function(_))
&& !scope.has_parent() && !parent_scope.has_parent()
{ {
self.write_output("main:")?; self.write_output("main:")?;
self.declared_main = true; self.declared_main = true;
@@ -1142,11 +1349,11 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
match expr.node { match expr.node {
Expression::Return(ret_expr) => { 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 // 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), // If the expression was a statement that returned a temp result (e.g. `1 + 2;` line),
// we must free it to avoid leaking registers. // we must free it to avoid leaking registers.
if let Some(comp_res) = result 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(()) Ok(())
} }
@@ -1190,6 +1401,14 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
debug!(self, "#returnValue") 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) => { VariableLocation::Stack(offset) => {
self.write_output(format!( self.write_output(format!(
"sub r{} sp {offset}", "sub r{} sp {offset}",
@@ -1252,6 +1471,23 @@ impl<'a, W: std::io::Write> Compiler<'a, W> {
scope.free_temp(name)?; 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( return Err(Error::Unknown(
format!("Unsupported `return` statement: {:?}", expr), format!("Unsupported `return` statement: {:?}", expr),

View File

@@ -3,6 +3,7 @@
// r1 - r7 : Temporary Variables // r1 - r7 : Temporary Variables
// r8 - r14 : Persistant Variables // r8 - r14 : Persistant Variables
use parser::tree_node::Literal;
use quick_error::quick_error; use quick_error::quick_error;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
@@ -43,6 +44,8 @@ pub enum VariableLocation {
Persistant(u8), Persistant(u8),
/// Represents a a stack offset (current stack - offset = variable loc) /// Represents a a stack offset (current stack - offset = variable loc)
Stack(u16), Stack(u16),
/// Represents a constant value and should be directly substituted as such.
Constant(Literal),
} }
pub struct VariableScope<'a> { pub struct VariableScope<'a> {
@@ -91,6 +94,8 @@ impl<'a> VariableScope<'a> {
pub fn scoped(parent: &'a VariableScope<'a>) -> Self { pub fn scoped(parent: &'a VariableScope<'a>) -> Self {
Self { Self {
parent: Option::Some(parent), parent: Option::Some(parent),
temporary_vars: parent.temporary_vars.clone(),
persistant_vars: parent.persistant_vars.clone(),
..Default::default() ..Default::default()
} }
} }
@@ -140,24 +145,48 @@ impl<'a> VariableScope<'a> {
Ok(var_location) Ok(var_location)
} }
pub fn get_location_of( pub fn define_const(
&mut self, &mut self,
var_name: impl Into<String>, var_name: impl Into<String>,
value: Literal,
) -> Result<VariableLocation, Error> { ) -> Result<VariableLocation, Error> {
let var_name = var_name.into(); let var_name = var_name.into();
let var = self if self.var_lookup_table.contains_key(&var_name) {
.var_lookup_table return Err(Error::DuplicateVariable(var_name));
.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)
} }
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<String>) -> Result<VariableLocation, Error> {
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 { pub fn has_parent(&self) -> bool {
@@ -180,7 +209,7 @@ impl<'a> VariableScope<'a> {
"Attempted to free a `let` variable.", "Attempted to free a `let` variable.",
))); )));
} }
VariableLocation::Stack(_) => {} _ => {}
}; };
Ok(()) Ok(())

View File

@@ -0,0 +1,6 @@
[package]
name = "helpers"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -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};
}

View File

@@ -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::<Vec<_>>()
.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::<Vec<_>>()
.join("\n")
.trim()
.to_string()
}
)
),*
]
}
}
};
}

View File

@@ -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"
);
};
}

View File

@@ -6,8 +6,10 @@ edition = "2024"
[dependencies] [dependencies]
quick-error = { workspace = true } quick-error = { workspace = true }
tokenizer = { path = "../tokenizer" } tokenizer = { path = "../tokenizer" }
helpers = { path = "../helpers" }
lsp-types = { workspace = true } lsp-types = { workspace = true }
[dev-dependencies] [dev-dependencies]
anyhow = { version = "1" } anyhow = { version = "1" }
pretty_assertions = "1.4"

View File

@@ -14,6 +14,10 @@ use tokenizer::{
}; };
use tree_node::*; use tree_node::*;
pub trait Documentation {
fn docs(&self) -> String;
}
#[macro_export] #[macro_export]
/// A macro to create a boxed value. /// A macro to create a boxed value.
macro_rules! boxed { macro_rules! boxed {
@@ -121,7 +125,6 @@ impl<'a> Parser<'a> {
} }
/// Calculates a Span from a given Token reference. /// 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 { fn token_to_span(t: &Token) -> Span {
let len = t.original_string.as_ref().map(|s| s.len()).unwrap_or(0); let len = t.original_string.as_ref().map(|s| s.len()).unwrap_or(0);
Span { Span {
@@ -149,7 +152,6 @@ impl<'a> Parser<'a> {
where where
F: FnOnce(&mut Self) -> Result<T, Error>, F: FnOnce(&mut Self) -> Result<T, Error>,
{ {
// 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() { let start_token = if self.current_token.is_some() {
self.current_token.clone() self.current_token.clone()
} else { } else {
@@ -163,7 +165,6 @@ impl<'a> Parser<'a> {
let node = parser(self)?; let node = parser(self)?;
// The end token is the current_token after parsing.
let end_token = self.current_token.as_ref(); let end_token = self.current_token.as_ref();
let (end_line, end_col) = end_token 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> { 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()?; self.assign_next()?;
while let Some(token) = &self.current_token { while let Some(token) = &self.current_token {
if token.token_type == TokenType::Symbol(Symbol::Semicolon) { 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()?; self.assign_next()?;
return Ok(()); 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 { match token.token_type {
TokenType::Keyword(Keyword::Fn) TokenType::Keyword(Keyword::Fn)
| TokenType::Keyword(Keyword::Let) | TokenType::Keyword(Keyword::Let)
@@ -231,7 +221,6 @@ impl<'a> Parser<'a> {
let mut expressions = Vec::<Spanned<Expression>>::new(); let mut expressions = Vec::<Spanned<Expression>>::new();
loop { loop {
// Check EOF without unwrapping error
match self.tokenizer.peek() { match self.tokenizer.peek() {
Ok(None) => break, Ok(None) => break,
Err(e) => { Err(e) => {
@@ -248,19 +237,13 @@ impl<'a> Parser<'a> {
Ok(None) => break, Ok(None) => break,
Err(e) => { Err(e) => {
self.errors.push(e); self.errors.push(e);
// Recover
if self.synchronize().is_err() { if self.synchronize().is_err() {
// If sync failed (e.g. EOF during sync), break
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_token_opt = self.tokenizer.peek().unwrap_or(None);
let (end_line, end_col) = end_token_opt let (end_line, end_col) = end_token_opt
.map(|tok| { .map(|tok| {
@@ -285,7 +268,6 @@ impl<'a> Parser<'a> {
pub fn parse(&mut self) -> Result<Option<Spanned<tree_node::Expression>>, Error> { pub fn parse(&mut self) -> Result<Option<Spanned<tree_node::Expression>>, Error> {
self.assign_next()?; self.assign_next()?;
// If assign_next hit EOF or error?
if self.current_token.is_none() { if self.current_token.is_none() {
return Ok(None); return Ok(None);
} }
@@ -317,15 +299,18 @@ impl<'a> Parser<'a> {
return Ok(None); 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!( if self_matches_peek!(
self, 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)?)); return Ok(Some(self.infix(lhs)?));
} else if self_matches_current!( } else if self_matches_current!(
self, 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))?; self.tokenizer.seek(SeekFrom::Current(-1))?;
return Ok(Some(self.infix(lhs)?)); return Ok(Some(self.infix(lhs)?));
@@ -334,6 +319,116 @@ impl<'a> Parser<'a> {
Ok(Some(lhs)) Ok(Some(lhs))
} }
/// Handles dot notation chains: x.y.z()
fn parse_postfix(
&mut self,
mut lhs: Spanned<Expression>,
) -> Result<Spanned<Expression>, 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::<Spanned<Expression>>::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<Option<Spanned<tree_node::Expression>>, Error> { fn unary(&mut self) -> Result<Option<Spanned<tree_node::Expression>>, Error> {
macro_rules! matches_keyword { macro_rules! matches_keyword {
($keyword:expr, $($pattern:pat),+) => { ($keyword:expr, $($pattern:pat),+) => {
@@ -357,10 +452,7 @@ impl<'a> Parser<'a> {
)); ));
} }
TokenType::Keyword(Keyword::Let) => { TokenType::Keyword(Keyword::Let) => Some(self.spanned(|p| p.declaration())?),
// declaration is wrapped in spanned inside the function, but expects 'let' to be current
Some(self.spanned(|p| p.declaration())?)
}
TokenType::Keyword(Keyword::Device) => { TokenType::Keyword(Keyword::Device) => {
let spanned_dev = self.spanned(|p| p.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) => { TokenType::Keyword(Keyword::Fn) => {
let spanned_fn = self.spanned(|p| p.function())?; let spanned_fn = self.spanned(|p| p.function())?;
Some(Spanned { Some(Spanned {
@@ -404,7 +505,6 @@ impl<'a> Parser<'a> {
TokenType::Keyword(Keyword::Break) => { TokenType::Keyword(Keyword::Break) => {
let span = self.current_span(); let span = self.current_span();
// make sure the next token is a semi-colon
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?; let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::Semicolon)) { if !token_matches!(next, TokenType::Symbol(Symbol::Semicolon)) {
return Err(Error::UnexpectedToken( 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) => { TokenType::Identifier(ref id) => {
let span = self.current_span(); let span = self.current_span();
Some(Spanned { Some(Spanned {
@@ -489,24 +579,36 @@ impl<'a> Parser<'a> {
} }
TokenType::Symbol(Symbol::LParen) => { TokenType::Symbol(Symbol::LParen) => {
// Priority handles its own spanning
self.spanned(|p| p.priority())?.node.map(|node| *node) self.spanned(|p| p.priority())?.node.map(|node| *node)
} }
TokenType::Symbol(Symbol::Minus) => { TokenType::Symbol(Symbol::Minus) => {
// Need to handle span manually because unary call is next
let start_span = self.current_span(); let start_span = self.current_span();
self.assign_next()?; self.assign_next()?;
let inner_expr = self.unary()?.ok_or(Error::UnexpectedEOF)?; 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 { let combined_span = Span {
start_line: start_span.start_line, start_line: start_span.start_line,
start_col: start_span.start_col, start_col: start_span.start_col,
end_line: inner_expr.span.end_line, end_line: inner_with_postfix.span.end_line,
end_col: inner_expr.span.end_col, end_col: inner_with_postfix.span.end_col,
}; };
Some(Spanned { Some(Spanned {
span: combined_span, 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(); let start_span = self.current_span();
self.assign_next()?; self.assign_next()?;
let inner_expr = self.unary()?.ok_or(Error::UnexpectedEOF)?; let inner_expr = self.unary()?.ok_or(Error::UnexpectedEOF)?;
let inner_with_postfix = self.parse_postfix(inner_expr)?;
let combined_span = Span { let combined_span = Span {
start_line: start_span.start_line, start_line: start_span.start_line,
start_col: start_span.start_col, start_col: start_span.start_col,
end_line: inner_expr.span.end_line, end_line: inner_with_postfix.span.end_line,
end_col: inner_expr.span.end_col, end_col: inner_with_postfix.span.end_col,
}; };
Some(Spanned { Some(Spanned {
span: combined_span, span: combined_span,
node: Expression::Logical(Spanned { node: Expression::Logical(Spanned {
span: combined_span, 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<Spanned<tree_node::Expression>, Error> { fn get_infix_child_node(&mut self) -> Result<Spanned<tree_node::Expression>, Error> {
let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?; 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(_) => { TokenType::Number(_) | TokenType::Boolean(_) => {
let lit = self.spanned(|p| p.literal())?; let lit = self.spanned(|p| p.literal())?;
Ok(Spanned { Spanned {
span: lit.span, span: lit.span,
node: Expression::Literal(lit), node: Expression::Literal(lit),
}) }
} }
TokenType::Identifier(ref ident) TokenType::Identifier(ref ident)
if !self_matches_peek!(self, TokenType::Symbol(Symbol::LParen)) => 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(); let span = self.current_span();
Ok(Spanned { Spanned {
span, span,
node: Expression::Variable(Spanned { node: Expression::Variable(Spanned {
span, span,
node: ident.clone(), node: ident.clone(),
}), }),
}) }
} }
TokenType::Symbol(Symbol::LParen) => Ok(*self TokenType::Symbol(Symbol::LParen) => *self
.spanned(|p| p.priority())? .spanned(|p| p.priority())?
.node .node
.ok_or(Error::UnexpectedEOF)?), .ok_or(Error::UnexpectedEOF)?,
TokenType::Identifier(_) TokenType::Identifier(_)
if self_matches_peek!(self, TokenType::Symbol(Symbol::LParen)) => if self_matches_peek!(self, TokenType::Symbol(Symbol::LParen)) =>
{ {
let inv = self.spanned(|p| p.invocation())?; let inv = self.spanned(|p| p.invocation())?;
Ok(Spanned { Spanned {
span: inv.span, span: inv.span,
node: Expression::Invocation(inv), node: Expression::Invocation(inv),
}) }
} }
TokenType::Symbol(Symbol::Minus) => { TokenType::Symbol(Symbol::Minus) => {
let start_span = self.current_span();
self.assign_next()?; self.assign_next()?;
let inner = self.get_infix_child_node()?; let inner = self.get_infix_child_node()?;
let span = Span { let span = Span {
@@ -586,13 +692,12 @@ impl<'a> Parser<'a> {
end_line: inner.span.end_line, end_line: inner.span.end_line,
end_col: inner.span.end_col, end_col: inner.span.end_col,
}; };
Ok(Spanned { Spanned {
span, span,
node: Expression::Negation(boxed!(inner)), node: Expression::Negation(boxed!(inner)),
}) }
} }
TokenType::Symbol(Symbol::LogicalNot) => { TokenType::Symbol(Symbol::LogicalNot) => {
let start_span = self.current_span();
self.assign_next()?; self.assign_next()?;
let inner = self.get_infix_child_node()?; let inner = self.get_infix_child_node()?;
let span = Span { let span = Span {
@@ -601,19 +706,25 @@ impl<'a> Parser<'a> {
end_line: inner.span.end_line, end_line: inner.span.end_line,
end_col: inner.span.end_col, end_col: inner.span.end_col,
}; };
Ok(Spanned { Spanned {
span, span,
node: Expression::Logical(Spanned { node: Expression::Logical(Spanned {
span, span,
node: LogicalExpression::Not(boxed!(inner)), node: LogicalExpression::Not(boxed!(inner)),
}), }),
}) }
} }
_ => Err(Error::UnexpectedToken( _ => {
self.current_span(), return Err(Error::UnexpectedToken(
current_token.clone(), 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<DeviceDeclarationExpression, Error> { fn device(&mut self) -> Result<DeviceDeclarationExpression, Error> {
@@ -665,39 +776,6 @@ impl<'a> Parser<'a> {
}) })
} }
fn assignment(&mut self) -> Result<AssignmentExpression, Error> {
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(&current_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<Expression>) -> Result<Spanned<Expression>, Error> { fn infix(&mut self, previous: Spanned<Expression>) -> Result<Spanned<Expression>, Error> {
let current_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?.clone(); let current_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?.clone();
@@ -708,7 +786,9 @@ impl<'a> Parser<'a> {
| Expression::Priority(_) | Expression::Priority(_)
| Expression::Literal(_) | Expression::Literal(_)
| Expression::Variable(_) | Expression::Variable(_)
| Expression::Negation(_) => {} | Expression::Negation(_)
| Expression::MemberAccess(_)
| Expression::MethodCall(_) => {}
_ => { _ => {
return Err(Error::InvalidSyntax( return Err(Error::InvalidSyntax(
self.current_span(), self.current_span(),
@@ -722,9 +802,10 @@ impl<'a> Parser<'a> {
let mut temp_token = current_token.clone(); let mut temp_token = current_token.clone();
// Include Assign in the operator loop
while token_matches!( while token_matches!(
temp_token, 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 { let operator = match temp_token.token_type {
TokenType::Symbol(s) => s, TokenType::Symbol(s) => s,
@@ -955,6 +1036,37 @@ impl<'a> Parser<'a> {
} }
operators.retain(|symbol| !matches!(symbol, Symbol::LogicalOr)); 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() { if expressions.len() != 1 || !operators.is_empty() {
return Err(Error::InvalidSyntax( return Err(Error::InvalidSyntax(
self.current_span(), self.current_span(),
@@ -1117,6 +1229,46 @@ impl<'a> Parser<'a> {
Ok(BlockExpression(expressions)) Ok(BlockExpression(expressions))
} }
fn const_declaration(&mut self) -> Result<ConstDeclarationExpression, Error> {
// 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<Expression, Error> { fn declaration(&mut self) -> Result<Expression, Error> {
let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?; let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?;
if !self_matches_current!(self, TokenType::Keyword(Keyword::Let)) { if !self_matches_current!(self, TokenType::Keyword(Keyword::Let)) {
@@ -1506,7 +1658,6 @@ impl<'a> Parser<'a> {
Literal::String(variable), Literal::String(variable),
))) )))
} }
// ... (implementing other syscalls similarly using patterns above)
"setOnDevice" => { "setOnDevice" => {
check_length(self, &invocation.arguments, 3)?; check_length(self, &invocation.arguments, 3)?;
let mut args = invocation.arguments.into_iter(); let mut args = invocation.arguments.into_iter();
@@ -1531,23 +1682,10 @@ impl<'a> Parser<'a> {
boxed!(variable), boxed!(variable),
))) )))
} }
_ => { _ => Err(Error::UnsupportedKeyword(
// For Math functions or unknown functions self.current_span(),
if SysCall::is_syscall(&invocation.name.node) { self.current_token.clone().ok_or(Error::UnexpectedEOF)?,
// 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)?,
))
}
}
} }
} }
} }

View File

@@ -1,73 +1,107 @@
use crate::tree_node::{Expression, Literal, Spanned};
use super::LiteralOrVariable; use super::LiteralOrVariable;
use crate::tree_node::{Expression, Literal, Spanned};
use helpers::prelude::*;
#[derive(Debug, PartialEq, Eq)] documented! {
pub enum Math { #[derive(Debug, PartialEq, Eq)]
/// Returns the angle in radians whose cosine is the specified number. pub enum Math {
/// ## In Game /// Returns the angle in radians whose cosine is the specified number.
/// `acos r? a(r?|num)` /// ## IC10
Acos(LiteralOrVariable), /// `acos r? a(r?|num)`
/// Returns the angle in radians whose sine is the specified number. /// ## Slang
/// ## In Game /// `(number|var).acos();`
/// `asin r? a(r?|num)` Acos(LiteralOrVariable),
Asin(LiteralOrVariable), /// Returns the angle in radians whose sine is the specified number.
/// Returns the angle in radians whose tangent is the specified number. /// ## IC10
/// ## In Game /// `asin r? a(r?|num)`
/// `atan r? a(r?|num)` /// ## Slang
Atan(LiteralOrVariable), /// `(number|var).asin();`
/// Returns the angle in radians whose tangent is the quotient of the specified numbers. Asin(LiteralOrVariable),
/// ## In Game /// Returns the angle in radians whose tangent is the specified number.
/// `atan2 r? a(r?|num) b(r?|num)` /// ## IC10
Atan2(LiteralOrVariable, LiteralOrVariable), /// `atan r? a(r?|num)`
/// Gets the absolute value of a number. /// ## Slang
/// ## In Game /// `(number|var).atan();`
/// `abs r? a(r?|num)` Atan(LiteralOrVariable),
Abs(LiteralOrVariable), /// Returns the angle in radians whose tangent is the quotient of the specified numbers.
/// Rounds a number up to the nearest whole number. /// ## IC10
/// ## In Game /// `atan2 r? a(r?|num) b(r?|num)`
/// `ceil r? a(r?|num)` /// ## Slang
Ceil(LiteralOrVariable), /// `(number|var).atan2((number|var));`
/// Returns the cosine of the specified angle in radians. Atan2(LiteralOrVariable, LiteralOrVariable),
/// ## In Game /// Gets the absolute value of a number.
/// cos r? a(r?|num) /// ## IC10
Cos(LiteralOrVariable), /// `abs r? a(r?|num)`
/// Rounds a number down to the nearest whole number. /// ## Slang
/// ## In Game /// `(number|var).abs();`
/// `floor r? a(r?|num)` Abs(LiteralOrVariable),
Floor(LiteralOrVariable), /// Rounds a number up to the nearest whole number.
/// Computes the natural logarithm of a number. /// ## IC10
/// ## In Game /// `ceil r? a(r?|num)`
/// `log r? a(r?|num)` /// ## Slang
Log(LiteralOrVariable), /// `(number|var).ceil();`
/// Computes the maximum of two numbers. Ceil(LiteralOrVariable),
/// ## In Game /// Returns the cosine of the specified angle in radians.
/// `max r? a(r?|num) b(r?|num)` /// ## IC10
Max(LiteralOrVariable, LiteralOrVariable), /// `cos r? a(r?|num)`
/// Computes the minimum of two numbers. /// ## Slang
/// ## In Game /// `(number|var).cos();`
/// `min r? a(r?|num) b(r?|num)` Cos(LiteralOrVariable),
Min(LiteralOrVariable, LiteralOrVariable), /// Rounds a number down to the nearest whole number.
/// Gets a random number between 0 and 1. /// ## IC10
/// ## In Game /// `floor r? a(r?|num)`
/// `rand r?` /// ## Slang
Rand, /// `(number|var).floor();`
/// Returns the sine of the specified angle in radians. Floor(LiteralOrVariable),
/// ## In Game /// Computes the natural logarithm of a number.
/// `sin r? a(r?|num)` /// ## IC10
Sin(LiteralOrVariable), /// `log r? a(r?|num)`
/// Computes the square root of a number. /// ## Slang
/// ## In Game /// `(number|var).log();`
/// `sqrt r? a(r?|num)` Log(LiteralOrVariable),
Sqrt(LiteralOrVariable), /// Computes the maximum of two numbers.
/// Returns the tangent of the specified angle in radians. /// ## IC10
/// ## In Game /// `max r? a(r?|num) b(r?|num)`
/// `tan r? a(r?|num)` /// ## Slang
Tan(LiteralOrVariable), /// `(number|var).max((number|var));`
/// Truncates a number by removing the decimal portion. Max(LiteralOrVariable, LiteralOrVariable),
/// ## In Game /// Computes the minimum of two numbers.
/// `trunc r? a(r?|num)` /// ## IC10
Trunc(LiteralOrVariable), /// `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 { impl std::fmt::Display for Math {
@@ -93,71 +127,76 @@ impl std::fmt::Display for Math {
} }
} }
#[derive(Debug, PartialEq, Eq)] documented! {
pub enum System { #[derive(Debug, PartialEq, Eq)]
/// Pauses execution for exactly 1 tick and then resumes. pub enum System {
/// ## In Game /// Pauses execution for exactly 1 tick and then resumes.
/// yield /// ## IC10
Yield, /// `yield`
/// Represents a function that can be called to sleep for a certain amount of time. /// ## Slang
/// ## In Game /// `yield();`
/// `sleep a(r?|num)` Yield,
Sleep(Box<Spanned<Expression>>), /// Represents a function that can be called to sleep for a certain amount of time.
/// Gets the in-game hash for a specific prefab name. /// ## IC10
/// ## In Game /// `sleep a(r?|num)`
/// `HASH("prefabName")` /// ## Slang
Hash(Literal), /// `sleep(number|var);`
/// Represents a function which loads a device variable into a register. Sleep(Box<Spanned<Expression>>),
/// ## In Game /// Gets the in-game hash for a specific prefab name.
/// `l r? d? var` /// ## IC10
/// ## Examples /// `HASH("prefabName")`
/// `l r0 d0 Setting` /// ## Slang
/// `l r1 d5 Pressure` /// `HASH("prefabName");`
LoadFromDevice(LiteralOrVariable, Literal), Hash(Literal),
/// Function which gets a LogicType from all connected network devices that match /// Represents a function which loads a device variable into a register.
/// the provided device hash and name, aggregating them via a batchMode /// ## IC10
/// ## In Game /// `l r? d? var`
/// lbn r? deviceHash nameHash logicType batchMode /// ## Slang
/// ## Examples /// `loadFromDevice(deviceType, "LogicType");`
/// lbn r0 HASH("StructureWallLight") HASH("wallLight") On Minimum LoadFromDevice(LiteralOrVariable, Literal),
LoadBatchNamed( /// Function which gets a LogicType from all connected network devices that match
LiteralOrVariable, /// the provided device hash and name, aggregating them via a batchMode
Box<Spanned<Expression>>, /// ## IC10
Literal, /// `lbn r? deviceHash nameHash logicType batchMode`
Literal, /// ## Slang
), /// `loadFromDeviceBatchedNamed(deviceHash, deviceName, "LogicType", "BatchMode");`
/// Loads a LogicType from all connected network devices, aggregating them via a LoadBatchNamed(
/// batchMode LiteralOrVariable,
/// ## In Game Box<Spanned<Expression>>,
/// lb r? deviceHash logicType batchMode Literal,
/// ## Examples Literal,
/// lb r0 HASH("StructureWallLight") On Minimum ),
LoadBatch(LiteralOrVariable, Literal, Literal), /// Loads a LogicType from all connected network devices, aggregating them via a
/// Represents a function which stores a setting into a specific device. /// batchMode
/// ## In Game /// ## IC10
/// `s d? logicType r?` /// `lb r? deviceHash logicType batchMode`
/// ## Example /// ## Slang
/// `s d0 Setting r0` /// `loadFromDeviceBatched(deviceHash, "Variable", "LogicType");`
SetOnDevice(LiteralOrVariable, Literal, Box<Spanned<Expression>>), LoadBatch(LiteralOrVariable, Literal, Literal),
/// Represents a function which stores a setting to all devices that match /// Represents a function which stores a setting into a specific device.
/// the given deviceHash /// ## IC10
/// ## In Game /// `s d? logicType r?`
/// `sb deviceHash logicType r?` /// ## Slang
/// ## Example /// `setOnDevice(deviceType, "Variable", (number|var));`
/// `sb HASH("Doors") Lock 1` SetOnDevice(LiteralOrVariable, Literal, Box<Spanned<Expression>>),
SetOnDeviceBatched(LiteralOrVariable, Literal, Box<Spanned<Expression>>), /// Represents a function which stores a setting to all devices that match
/// Represents a function which stores a setting to all devices that match /// the given deviceHash
/// both the given deviceHash AND the given nameHash /// ## IC10
/// ## In Game /// `sb deviceHash logicType r?`
/// `sbn deviceHash nameHash logicType r?` SetOnDeviceBatched(LiteralOrVariable, Literal, Box<Spanned<Expression>>),
/// ## Example /// Represents a function which stores a setting to all devices that match
/// `sbn HASH("Doors") HASH("Exterior") Lock 1` /// both the given deviceHash AND the given nameHash
SetOnDeviceBatchedNamed( /// ## IC10
LiteralOrVariable, /// `sbn deviceHash nameHash logicType r?`
LiteralOrVariable, /// ## Slang
Literal, /// `setOnDeviceBatchedNamed(deviceType, nameHash, "LogicType", (number|var))`
Box<Spanned<Expression>>, SetOnDeviceBatchedNamed(
), LiteralOrVariable,
LiteralOrVariable,
Literal,
Box<Spanned<Expression>>,
),
}
} }
impl std::fmt::Display for System { impl std::fmt::Display for System {
@@ -190,6 +229,22 @@ pub enum SysCall {
Math(Math), 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 { impl std::fmt::Display for SysCall {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@@ -201,32 +256,6 @@ impl std::fmt::Display for SysCall {
impl SysCall { impl SysCall {
pub fn is_syscall(identifier: &str) -> bool { pub fn is_syscall(identifier: &str) -> bool {
matches!( tokenizer::token::is_syscall(identifier)
identifier,
"yield"
| "sleep"
| "hash"
| "loadFromDevice"
| "setOnDevice"
| "setOnDeviceBatched"
| "setOnDeviceBatchedNamed"
| "acos"
| "asin"
| "atan"
| "atan2"
| "abs"
| "ceil"
| "cos"
| "floor"
| "log"
| "max"
| "min"
| "rand"
| "sin"
| "sqrt"
| "tan"
| "trunc"
)
} }
} }

View File

@@ -1,7 +1,7 @@
#[macro_export] #[macro_export]
macro_rules! parser { macro_rules! parser {
($input:expr) => { ($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::Parser;
use super::Tokenizer; use super::Tokenizer;
use anyhow::Result; use anyhow::Result;
use pretty_assertions::assert_eq;
#[test] #[test]
fn test_unsupported_keywords() -> Result<()> { fn test_unsupported_keywords() -> Result<()> {
@@ -31,7 +32,7 @@ fn test_declarations() -> Result<()> {
// The below line should fail // The below line should fail
let y = 234 let y = 234
"#; "#;
let tokenizer = Tokenizer::from(input.to_owned()); let tokenizer = Tokenizer::from(input);
let mut parser = Parser::new(tokenizer); let mut parser = Parser::new(tokenizer);
let expression = parser.parse()?.unwrap(); let expression = parser.parse()?.unwrap();
@@ -43,6 +44,36 @@ fn test_declarations() -> Result<()> {
Ok(()) 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] #[test]
fn test_function_expression() -> Result<()> { fn test_function_expression() -> Result<()> {
let input = r#" 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 mut parser = Parser::new(tokenizer);
let expression = parser.parse()?.unwrap(); let expression = parser.parse()?.unwrap();
@@ -71,7 +102,7 @@ fn test_function_invocation() -> Result<()> {
add(); add();
"#; "#;
let tokenizer = Tokenizer::from(input.to_owned()); let tokenizer = Tokenizer::from(input);
let mut parser = Parser::new(tokenizer); let mut parser = Parser::new(tokenizer);
let expression = parser.parse()?.unwrap(); let expression = parser.parse()?.unwrap();
@@ -87,7 +118,7 @@ fn test_priority_expression() -> Result<()> {
let x = (4); let x = (4);
"#; "#;
let tokenizer = Tokenizer::from(input.to_owned()); let tokenizer = Tokenizer::from(input);
let mut parser = Parser::new(tokenizer); let mut parser = Parser::new(tokenizer);
let expression = parser.parse()?.unwrap(); let expression = parser.parse()?.unwrap();
@@ -99,16 +130,16 @@ fn test_priority_expression() -> Result<()> {
#[test] #[test]
fn test_binary_expression() -> Result<()> { 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()); 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()); 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()); 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()); assert_eq!("((5 - 2) * 10)", expr.to_string());
Ok(()) Ok(())

View File

@@ -74,13 +74,13 @@ impl std::fmt::Display for LogicalExpression {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct AssignmentExpression { pub struct AssignmentExpression {
pub identifier: Spanned<String>, pub assignee: Box<Spanned<Expression>>,
pub expression: Box<Spanned<Expression>>, pub expression: Box<Spanned<Expression>>,
} }
impl std::fmt::Display for AssignmentExpression { impl std::fmt::Display for AssignmentExpression {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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<Spanned<Expression>>,
pub member: Spanned<String>,
}
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<Spanned<Expression>>,
pub method: Spanned<String>,
pub arguments: Vec<Spanned<Expression>>,
}
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::<Vec<String>>()
.join(", ")
)
}
}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum LiteralOrVariable { pub enum LiteralOrVariable {
Literal(Literal), Literal(Literal),
@@ -160,6 +195,18 @@ impl std::fmt::Display for LiteralOrVariable {
} }
} }
#[derive(Debug, PartialEq, Eq)]
pub struct ConstDeclarationExpression {
pub name: Spanned<String>,
pub value: Spanned<Literal>,
}
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)] #[derive(Debug, PartialEq, Eq)]
pub struct DeviceDeclarationExpression { pub struct DeviceDeclarationExpression {
/// any variable-like name /// any variable-like name
@@ -281,6 +328,7 @@ pub enum Expression {
Binary(Spanned<BinaryExpression>), Binary(Spanned<BinaryExpression>),
Block(Spanned<BlockExpression>), Block(Spanned<BlockExpression>),
Break(Span), Break(Span),
ConstDeclaration(Spanned<ConstDeclarationExpression>),
Continue(Span), Continue(Span),
Declaration(Spanned<String>, Box<Spanned<Expression>>), Declaration(Spanned<String>, Box<Spanned<Expression>>),
DeviceDeclaration(Spanned<DeviceDeclarationExpression>), DeviceDeclaration(Spanned<DeviceDeclarationExpression>),
@@ -290,6 +338,8 @@ pub enum Expression {
Literal(Spanned<Literal>), Literal(Spanned<Literal>),
Logical(Spanned<LogicalExpression>), Logical(Spanned<LogicalExpression>),
Loop(Spanned<LoopExpression>), Loop(Spanned<LoopExpression>),
MemberAccess(Spanned<MemberAccessExpression>),
MethodCall(Spanned<MethodCallExpression>),
Negation(Box<Spanned<Expression>>), Negation(Box<Spanned<Expression>>),
Priority(Box<Spanned<Expression>>), Priority(Box<Spanned<Expression>>),
Return(Box<Spanned<Expression>>), Return(Box<Spanned<Expression>>),
@@ -305,6 +355,7 @@ impl std::fmt::Display for Expression {
Expression::Binary(e) => write!(f, "{}", e), Expression::Binary(e) => write!(f, "{}", e),
Expression::Block(e) => write!(f, "{}", e), Expression::Block(e) => write!(f, "{}", e),
Expression::Break(_) => write!(f, "break"), Expression::Break(_) => write!(f, "break"),
Expression::ConstDeclaration(e) => write!(f, "{}", e),
Expression::Continue(_) => write!(f, "continue"), Expression::Continue(_) => write!(f, "continue"),
Expression::Declaration(id, e) => write!(f, "(let {} = {})", id, e), Expression::Declaration(id, e) => write!(f, "(let {} = {})", id, e),
Expression::DeviceDeclaration(e) => write!(f, "{}", e), Expression::DeviceDeclaration(e) => write!(f, "{}", e),
@@ -314,6 +365,8 @@ impl std::fmt::Display for Expression {
Expression::Literal(l) => write!(f, "{}", l), Expression::Literal(l) => write!(f, "{}", l),
Expression::Logical(e) => write!(f, "{}", e), Expression::Logical(e) => write!(f, "{}", e),
Expression::Loop(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::Negation(e) => write!(f, "(-{})", e),
Expression::Priority(e) => write!(f, "({})", e), Expression::Priority(e) => write!(f, "({})", e),
Expression::Return(e) => write!(f, "(return {})", e), Expression::Return(e) => write!(f, "(return {})", e),

View File

@@ -7,6 +7,7 @@ edition = "2024"
rust_decimal = { workspace = true } rust_decimal = { workspace = true }
quick-error = { workspace = true } quick-error = { workspace = true }
lsp-types = { workspace = true } lsp-types = { workspace = true }
helpers = { path = "../helpers" }
[dev-dependencies] [dev-dependencies]
anyhow = { version = "^1" } anyhow = { version = "^1" }

View File

@@ -199,7 +199,7 @@ impl<'a> Tokenizer<'a> {
.tokenize_symbol(next_char, start_line, start_col) .tokenize_symbol(next_char, start_line, start_col)
.map(Some); .map(Some);
} }
char if char.is_alphabetic() => { char if char.is_alphabetic() || char == '_' => {
return self return self
.tokenize_keyword_or_identifier(next_char, start_line, start_col) .tokenize_keyword_or_identifier(next_char, start_line, start_col)
.map(Some); .map(Some);
@@ -439,14 +439,15 @@ impl<'a> Tokenizer<'a> {
}}; }};
} }
macro_rules! next_ws { 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 buffer = String::with_capacity(16);
let mut looped_char = Some(first_char); let mut looped_char = Some(first_char);
while let Some(next_char) = looped_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; break;
} }
buffer.push(next_char); buffer.push(next_char);
@@ -463,6 +464,7 @@ impl<'a> Tokenizer<'a> {
"break" if next_ws!() => keyword!(Break), "break" if next_ws!() => keyword!(Break),
"while" if next_ws!() => keyword!(While), "while" if next_ws!() => keyword!(While),
"continue" if next_ws!() => keyword!(Continue), "continue" if next_ws!() => keyword!(Continue),
"const" if next_ws!() => keyword!(Const),
"true" if next_ws!() => { "true" if next_ws!() => {
return Ok(Token::new( return Ok(Token::new(
TokenType::Boolean(true), TokenType::Boolean(true),
@@ -837,7 +839,9 @@ mod tests {
#[test] #[test]
fn test_keyword_parse() -> Result<()> { 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![ let expected_tokens = vec![
TokenType::Keyword(Keyword::Let), TokenType::Keyword(Keyword::Let),
@@ -846,6 +850,9 @@ mod tests {
TokenType::Keyword(Keyword::Else), TokenType::Keyword(Keyword::Else),
TokenType::Keyword(Keyword::Return), TokenType::Keyword(Keyword::Return),
TokenType::Keyword(Keyword::Enum), TokenType::Keyword(Keyword::Enum),
TokenType::Keyword(Keyword::Continue),
TokenType::Keyword(Keyword::Break),
TokenType::Keyword(Keyword::Const),
]; ];
for expected_token in expected_tokens { for expected_token in expected_tokens {
@@ -859,7 +866,7 @@ mod tests {
#[test] #[test]
fn test_identifier_parse() -> Result<()> { 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(); let token = tokenizer.next_token()?.unwrap();
assert_eq!(token.token_type, TokenType::Keyword(Keyword::Fn)); assert_eq!(token.token_type, TokenType::Keyword(Keyword::Fn));
@@ -868,6 +875,13 @@ mod tests {
token.token_type, token.token_type,
TokenType::Identifier(String::from("test")) 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(()) Ok(())
} }

View File

@@ -1,5 +1,15 @@
use helpers::prelude::*;
use rust_decimal::Decimal; 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)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct Token { pub struct Token {
/// The type of the token /// The type of the token
@@ -87,17 +97,56 @@ pub enum TokenType {
EOF, 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<TokenType> for u32 { impl From<TokenType> for u32 {
fn from(value: TokenType) -> Self { fn from(value: TokenType) -> Self {
use TokenType::*;
match value { match value {
String(_) => 1, TokenType::String(_) => 1,
Number(_) => 2, TokenType::Number(_) => 2,
Boolean(_) => 3, TokenType::Boolean(_) => 3,
Keyword(_) => 4, TokenType::Keyword(k) => match k {
Identifier(_) => 5, Keyword::If
Symbol(_) => 6, | Keyword::Else
EOF => 0, | 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)] documented! {
pub enum Keyword { #[derive(Debug, PartialEq, Hash, Eq, Clone, Copy)]
/// Represents the `continue` keyword pub enum Keyword {
Continue, /// Represents the `continue` keyword. This will allow you to bypass the current iteration in a loop and start the next one.
/// Represents the `let` keyword /// ## Example
Let, /// ```
/// Represents the `fn` keyword /// let item = 0;
Fn, /// loop {
/// Represents the `if` keyword /// if (item % 2 == 0) {
If, /// // This will NOT increment `item` and will continue with the next iteration of the
/// Represents the `device` keyword. Useful for defining a device at a specific address (ex. d0, d1, d2, etc.) /// // loop
Device, /// continue;
/// Represents the `else` keyword /// }
Else, /// item = item + 1;
/// Represents the `return` keyword /// }
Return, /// ```
/// Represents the `enum` keyword Continue,
Enum, /// Prepresents the `const` keyword. This allows you to define a variable that will never
/// Represents the `loop` keyword /// change throughout the lifetime of the program, similar to `define` in IC10. If you are
Loop, /// not planning on mutating the variable (changing it), it is recommend you store it as a
/// Represents the `break` keyword /// const, as the compiler will not assign it to a register or stack variable.
Break, ///
/// Represents the `while` keyword /// ## Example
While, /// ```
/// 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,
}
} }

View File

@@ -1,5 +1,6 @@
use compiler::Compiler; use compiler::Compiler;
use parser::Parser; use helpers::Documentation;
use parser::{sys_call::SysCall, Parser};
use safer_ffi::prelude::*; use safer_ffi::prelude::*;
use std::io::BufWriter; use std::io::BufWriter;
use tokenizer::{ use tokenizer::{
@@ -26,6 +27,13 @@ pub struct FfiRange {
end_line: u32, end_line: u32,
} }
#[derive_ReprC]
#[repr(C)]
pub struct FfiDocumentedItem {
item_name: safer_ffi::String,
docs: safer_ffi::String,
}
impl From<lsp_types::Range> for FfiRange { impl From<lsp_types::Range> for FfiRange {
fn from(value: lsp_types::Range) -> Self { fn from(value: lsp_types::Range) -> Self {
Self { Self {
@@ -76,6 +84,11 @@ pub fn free_string(s: safer_ffi::String) {
drop(s) drop(s)
} }
#[ffi_export]
pub fn free_docs_vec(v: safer_ffi::Vec<FfiDocumentedItem>) {
drop(v)
}
/// C# handles strings as UTF16. We do NOT want to allocate that memory in C# because /// 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. /// 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 /// 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<Ff
column: column as i32, column: column as i32,
error: "".into(), error: "".into(),
length: (original_string.unwrap_or_default().len()) as i32, length: (original_string.unwrap_or_default().len()) as i32,
tooltip: token_type.docs().into(),
token_kind: token_type.into(), token_kind: token_type.into(),
tooltip: "".into(),
}), }),
} }
} }
@@ -183,3 +196,26 @@ pub fn diagnose_source(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<
res.unwrap_or(vec![].into()) res.unwrap_or(vec![].into())
} }
#[ffi_export]
pub fn get_docs() -> safer_ffi::Vec<FfiDocumentedItem> {
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::<Vec<_>>()
.into()
}