15 Commits

Author SHA1 Message Date
b8521917b8 First pass getting user documentation in the IDE
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 38s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2026-01-02 19:14:58 -07:00
4ff0ff1b66 Track various symbols in the parse stage of Slang 2026-01-02 17:06:43 -07:00
6dc4342ac3 First pass getting deeper LSP into IDE 2026-01-02 16:57:06 -07:00
2070c2e4ca CLI error handling
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 37s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2026-01-02 16:44:38 -07:00
4c704b8960 Attempt to fold constants when folding expressions
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 37s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2026-01-02 03:06:37 -07:00
3c7300d2e1 Improve unexpectedEOF tracking
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 37s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2026-01-01 16:51:35 -07:00
647c3d29d5 Improve binary folding 2026-01-01 15:57:35 -07:00
e3a38ec110 Remove MD file 2026-01-01 14:01:34 -07:00
1f98ab8d75 Removed unneeded parenthesis 2026-01-01 14:01:14 -07:00
76c5df5dc2 Bitwise folding with hash() as well 2026-01-01 14:00:10 -07:00
0999ae8aed Array indexing support first pass 2026-01-01 13:46:04 -07:00
072a6b9ea6 Merge master 2026-01-01 12:50:02 -07:00
d8fe9a0d7d Merge pull request 'optimizer-bug' (#14) from optimizer-bug into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 36s
CI/CD Pipeline / build (push) Successful in 1m44s
CI/CD Pipeline / release (push) Successful in 4s
Reviewed-on: #14
2026-01-01 12:41:11 -07:00
089fe46d36 update changelog and version bump
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 37s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2026-01-01 12:38:53 -07:00
14c641797a Fixed optimizer bug where certain syscalls were not included in register liveness analysis 2026-01-01 12:35:57 -07:00
42 changed files with 2367 additions and 409 deletions

View File

@@ -1,5 +1,13 @@
# Changelog
[0.5.1]
- Fixed optimizer bug where `StoreBatch` and `StoreBatchNamed` instructions
were not recognized as reading operands, causing incorrect elimination of
necessary device property loads
- Added comprehensive register read tracking for `StoreSlot`, `JumpRelative`,
and `Alias` instructions in the optimizer
[0.5.0]
- Added full tuple support: declarations, assignments, and returns

View File

@@ -2,7 +2,7 @@
<ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Slang</Name>
<Author>JoeDiertay</Author>
<Version>0.5.0</Version>
<Version>0.5.1</Version>
<Description>
[h1]Slang: High-Level Programming for Stationeers[/h1]

View File

@@ -207,4 +207,34 @@ public static unsafe class SlangExtensions
Ffi.free_docs_vec(vec);
return toReturn;
}
public static unsafe List<Symbol> ToList(this Vec_FfiSymbolInfo_t vec)
{
var toReturn = new List<Symbol>((int)vec.len);
var currentPtr = vec.ptr;
for (int i = 0; i < (int)vec.len; i++)
{
var item = currentPtr[i];
toReturn.Add(
new Slang.Symbol
{
Name = item.name.AsString(),
Kind = (SymbolKind)item.kind_data.kind,
Span = new Slang.Range
{
StartLine = item.span.start_line,
StartCol = item.span.start_col,
EndLine = item.span.end_line,
EndCol = item.span.end_col,
},
Description = item.description.AsString(),
}
);
}
return toReturn;
}
}

View File

@@ -147,6 +147,51 @@ public unsafe partial class Ffi {
slice_ref_uint16_t input);
}
[StructLayout(LayoutKind.Sequential, Size = 12)]
public unsafe struct FfiSymbolKindData_t {
public UInt32 kind;
public UInt32 arg_count;
public UInt32 syscall_type;
}
[StructLayout(LayoutKind.Sequential, Size = 80)]
public unsafe struct FfiSymbolInfo_t {
public Vec_uint8_t name;
public FfiSymbolKindData_t kind_data;
public FfiRange_t span;
public Vec_uint8_t description;
}
/// <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_FfiSymbolInfo_t {
public FfiSymbolInfo_t * ptr;
public UIntPtr len;
public UIntPtr cap;
}
[StructLayout(LayoutKind.Sequential, Size = 48)]
public unsafe struct FfiDiagnosticsAndSymbols_t {
public Vec_FfiDiagnostic_t diagnostics;
public Vec_FfiSymbolInfo_t symbols;
}
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
FfiDiagnosticsAndSymbols_t diagnose_source_with_symbols (
slice_ref_uint16_t input);
}
[StructLayout(LayoutKind.Sequential, Size = 48)]
public unsafe struct FfiDocumentedItem_t {
public Vec_uint8_t item_name;
@@ -184,6 +229,12 @@ public unsafe partial class Ffi {
Vec_FfiDiagnostic_t v);
}
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
void free_ffi_diagnostics_and_symbols (
FfiDiagnosticsAndSymbols_t v);
}
[StructLayout(LayoutKind.Sequential, Size = 64)]
public unsafe struct FfiToken_t {
public Vec_uint8_t tooltip;

View File

@@ -171,18 +171,17 @@ public class SlangFormatter : ICodeFormatter
return;
// Running this potentially CPU intensive work on a background thread.
var dict = await Task.Run(
var (diagnostics, symbols) = await Task.Run(
() =>
{
return Marshal
.DiagnoseSource(inputSrc)
.GroupBy(d => d.Range.StartLine)
.ToDictionary(g => g.Key);
return Marshal.DiagnoseSourceWithSymbols(inputSrc);
},
cancellationToken
);
ApplyDiagnostics(dict);
var dict = diagnostics.GroupBy(d => d.Range.StartLine).ToDictionary(g => g.Key);
ApplyDiagnosticsAndSymbols(dict, symbols);
// If we have valid code, update the IC10 output
if (dict.Count > 0)
@@ -266,11 +265,11 @@ public class SlangFormatter : ICodeFormatter
}
/// <summary>
/// Takes diagnostics from the Rust FFI compiler and applies it as semantic tokens to the
/// Takes diagnostics and symbols from the Rust FFI compiler and applies them as semantic tokens to the
/// source in this editor.
/// This runs on the Main Thread
/// </summary>
private void ApplyDiagnostics(Dictionary<uint, IGrouping<uint, Diagnostic>> dict)
private void ApplyDiagnosticsAndSymbols(Dictionary<uint, IGrouping<uint, Diagnostic>> dict, List<Symbol> symbols)
{
HashSet<uint> linesToRefresh;
@@ -289,6 +288,12 @@ public class SlangFormatter : ICodeFormatter
{
linesToRefresh = new HashSet<uint>(dict.Keys);
linesToRefresh.UnionWith(_linesWithErrors);
// Also add lines with symbols that may have been modified
foreach (var symbol in symbols)
{
linesToRefresh.Add(symbol.Span.StartLine);
}
}
_lastLineCount = this.Lines.Count;
@@ -328,9 +333,49 @@ public class SlangFormatter : ICodeFormatter
}
}
// 3. Add symbol tooltips for symbols on this line
foreach (var symbol in symbols)
{
if (symbol.Span.StartLine == lineIndex)
{
var column = (int)symbol.Span.StartCol;
var length = Math.Max(1, (int)(symbol.Span.EndCol - symbol.Span.StartCol));
// If there's already a token at this position (from syntax highlighting), use it
// Otherwise, create a new token for the symbol
if (allTokensDict.ContainsKey(column))
{
// Update existing token with symbol tooltip
var existingToken = allTokensDict[column];
allTokensDict[column] = new SemanticToken(
line: existingToken.Line,
column: existingToken.Column,
length: existingToken.Length,
type: existingToken.Type,
style: existingToken.Style,
data: symbol.Description, // Use symbol description as tooltip
isError: existingToken.IsError
);
}
else
{
// Create new token for symbol
allTokensDict[column] = new SemanticToken(
line: (int)lineIndex,
column,
length,
type: 0,
style: ColorIdentifier,
data: symbol.Description,
isError: false
);
}
}
}
var allTokens = allTokensDict.Values.ToList();
// 3. Update the line (this clears existing tokens and uses the list we just built)
// 4. Update the line (this clears existing tokens and uses the list we just built)
line.Update(allTokens);
ReattachMetadata(line, allTokens);
@@ -339,6 +384,16 @@ public class SlangFormatter : ICodeFormatter
_linesWithErrors = new HashSet<uint>(dict.Keys);
}
/// <summary>
/// Takes diagnostics from the Rust FFI compiler and applies it as semantic tokens to the
/// source in this editor.
/// This runs on the Main Thread
/// </summary>
private void ApplyDiagnostics(Dictionary<uint, IGrouping<uint, Diagnostic>> dict)
{
ApplyDiagnosticsAndSymbols(dict, new List<Symbol>());
}
// Helper to map SemanticToken data (tooltips/errors) back to the tokens in the line
private void ReattachMetadata(StyledLine line, List<SemanticToken> semanticTokens)
{

View File

@@ -47,6 +47,33 @@ public struct SourceMapEntry
}
}
public struct Symbol
{
public string Name;
public Range Span;
public SymbolKind Kind;
public string Description;
public override string ToString()
{
return $"{Kind}: {Name} at {Span}";
}
}
public enum SymbolKind
{
Function = 0,
Syscall = 1,
Variable = 2,
}
public struct SymbolData
{
public uint Kind;
public uint ArgCount;
public uint SyscallType; // 0=System, 1=Math
}
public static class Marshal
{
private static IntPtr _libraryHandle = IntPtr.Zero;
@@ -164,6 +191,59 @@ public static class Marshal
}
}
public static unsafe (List<Diagnostic>, List<Symbol>) DiagnoseSourceWithSymbols(string inputString)
{
if (string.IsNullOrEmpty(inputString) || !EnsureLibLoaded())
{
return (new(), new());
}
fixed (char* ptrInput = inputString)
{
var input = new slice_ref_uint16_t
{
ptr = (ushort*)ptrInput,
len = (UIntPtr)inputString.Length,
};
var result = Ffi.diagnose_source_with_symbols(input);
// Convert diagnostics
var diagnostics = result.diagnostics.ToList();
// Convert symbols
var symbols = new List<Symbol>();
var symbolPtr = result.symbols.ptr;
var symbolCount = (int)result.symbols.len;
for (int i = 0; i < symbolCount; i++)
{
var ffiSymbol = symbolPtr[i];
var kind = (SymbolKind)ffiSymbol.kind_data.kind;
// Use the actual description from the FFI (includes doc comments and syscall docs)
var description = ffiSymbol.description.AsString();
symbols.Add(new Symbol
{
Name = ffiSymbol.name.AsString(),
Kind = kind,
Span = new Range(
ffiSymbol.span.start_line,
ffiSymbol.span.start_col,
ffiSymbol.span.end_line,
ffiSymbol.span.end_col
),
Description = description,
});
}
Ffi.free_ffi_diagnostics_and_symbols(result);
return (diagnostics, symbols);
}
}
public static unsafe List<SemanticToken> TokenizeLine(string inputString)
{
if (string.IsNullOrEmpty(inputString) || !EnsureLibLoaded())

View File

@@ -39,7 +39,7 @@ namespace Slang
{
public const string PluginGuid = "com.biddydev.slang";
public const string PluginName = "Slang";
public const string PluginVersion = "0.5.0";
public const string PluginVersion = "0.5.1";
private static Harmony? _harmony;

View File

@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<AssemblyName>StationeersSlang</AssemblyName>
<Description>Slang Compiler Bridge</Description>
<Version>0.5.0</Version>
<Version>0.5.1</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
</PropertyGroup>

View File

@@ -173,7 +173,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.112",
]
[[package]]
@@ -224,9 +224,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.53"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
"clap_derive",
@@ -234,9 +234,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.53"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [
"anstream",
"anstyle",
@@ -253,7 +253,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.112",
]
[[package]]
@@ -523,9 +523,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.15"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
@@ -571,7 +571,7 @@ dependencies = [
"regex-automata",
"regex-syntax",
"rustc_version",
"syn 2.0.111",
"syn 2.0.112",
]
[[package]]
@@ -726,9 +726,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.103"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
dependencies = [
"unicode-ident",
]
@@ -909,12 +909,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "safer-ffi"
version = "0.1.13"
@@ -992,20 +986,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.112",
]
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -1016,7 +1010,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.112",
]
[[package]]
@@ -1039,7 +1033,7 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "slang"
version = "0.5.0"
version = "0.5.1"
dependencies = [
"anyhow",
"clap",
@@ -1108,9 +1102,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.111"
version = "2.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
dependencies = [
"proc-macro2",
"quote",
@@ -1153,7 +1147,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.112",
]
[[package]]
@@ -1185,9 +1179,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.4+spec-1.0.0"
version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe3cea6b2aa3b910092f6abd4053ea464fab5f9c170ba5e9a6aead16ec4af2b6"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
@@ -1206,9 +1200,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.5+spec-1.0.0"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c03bee5ce3696f31250db0bbaff18bc43301ce0e8db2ed1f07cbb2acf89984c"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
dependencies = [
"winnow",
]
@@ -1303,7 +1297,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.112",
"wasm-bindgen-shared",
]
@@ -1471,5 +1465,11 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
"syn 2.0.112",
]
[[package]]
name = "zmij"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd"

View File

@@ -1,6 +1,6 @@
[package]
name = "slang"
version = "0.5.0"
version = "0.5.1"
edition = "2021"
[workspace]

View File

@@ -1,6 +1,8 @@
pub mod symbols;
#[cfg(test)]
mod test;
mod v1;
mod variable_manager;
pub use symbols::{CompilationMetadata, SymbolInfo, SymbolKind, SyscallType};
pub use v1::{CompilationResult, Compiler, CompilerConfig, Error};

View File

@@ -0,0 +1,343 @@
use helpers::Span;
use std::borrow::Cow;
/// Represents a symbol (function, syscall, variable, etc.) that can be referenced in code.
/// Designed to be LSP-compatible for easy integration with language servers.
#[derive(Debug, Clone)]
pub struct SymbolInfo<'a> {
/// The name of the symbol
pub name: Cow<'a, str>,
/// The kind of symbol and associated metadata
pub kind: SymbolKind<'a>,
/// The source location of this symbol (for IDE features)
pub span: Option<Span>,
/// Optional description for tooltips and documentation
pub description: Option<Cow<'a, str>>,
}
impl<'a> SymbolInfo<'a> {
/// Converts to an LSP SymbolInformation for protocol compatibility.
pub fn to_lsp_symbol_information(&self, uri: lsp_types::Uri) -> lsp_types::SymbolInformation {
lsp_types::SymbolInformation {
name: self.name.to_string(),
kind: self.kind.to_lsp_symbol_kind(),
#[allow(deprecated)]
deprecated: None,
location: lsp_types::Location {
uri,
range: self.span.as_ref().map(|s| (*s).into()).unwrap_or_default(),
},
container_name: None,
tags: None,
}
}
/// Converts to an LSP CompletionItem for autocomplete.
pub fn to_lsp_completion_item(&self) -> lsp_types::CompletionItem {
lsp_types::CompletionItem {
label: self.name.to_string(),
kind: Some(self.kind.to_lsp_completion_kind()),
documentation: self
.description
.as_ref()
.map(|d| lsp_types::Documentation::String(d.to_string())),
detail: Some(self.kind.detail_string()),
..Default::default()
}
}
}
/// Discriminates between different kinds of symbols.
#[derive(Debug, Clone)]
pub enum SymbolKind<'a> {
/// A user-defined function
Function {
/// Names of parameters in order
parameters: Vec<Cow<'a, str>>,
/// Type hint for the return type (if applicable)
return_type: Option<Cow<'a, str>>,
},
/// A system or math syscall
Syscall {
/// Whether it's a System or Math syscall
syscall_type: SyscallType,
/// Number of expected arguments
argument_count: usize,
},
/// A variable declaration
Variable {
/// Type hint for the variable (if applicable)
type_hint: Option<Cow<'a, str>>,
},
}
impl<'a> SymbolKind<'a> {
/// Converts to LSP SymbolKind for protocol compatibility.
fn to_lsp_symbol_kind(&self) -> lsp_types::SymbolKind {
match self {
SymbolKind::Function { .. } => lsp_types::SymbolKind::FUNCTION,
SymbolKind::Syscall { .. } => lsp_types::SymbolKind::FUNCTION, // Syscalls are function-like
SymbolKind::Variable { .. } => lsp_types::SymbolKind::VARIABLE,
}
}
/// Converts to LSP CompletionItemKind for autocomplete filtering.
fn to_lsp_completion_kind(&self) -> lsp_types::CompletionItemKind {
match self {
SymbolKind::Function { .. } => lsp_types::CompletionItemKind::FUNCTION,
SymbolKind::Syscall { .. } => lsp_types::CompletionItemKind::FUNCTION,
SymbolKind::Variable { .. } => lsp_types::CompletionItemKind::VARIABLE,
}
}
/// Returns a human-readable detail string for display in IDEs.
fn detail_string(&self) -> String {
match self {
SymbolKind::Function {
parameters,
return_type,
} => {
let params = parameters
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(", ");
let ret = return_type
.as_ref()
.map(|t| format!(" -> {}", t))
.unwrap_or_default();
format!("fn({}){}", params, ret)
}
SymbolKind::Syscall {
syscall_type,
argument_count,
} => {
format!(
"{}(... {} args)",
match syscall_type {
SyscallType::System => "syscall",
SyscallType::Math => "math",
},
argument_count
)
}
SymbolKind::Variable { type_hint } => type_hint
.as_ref()
.map(|t| t.to_string())
.unwrap_or_else(|| "var".to_string()),
}
}
}
/// Distinguishes between System and Math syscalls.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyscallType {
System,
Math,
}
/// Metadata collected during compilation, including all referenced symbols.
#[derive(Debug, Default)]
pub struct CompilationMetadata<'a> {
/// All symbols encountered during compilation (functions, syscalls, variables)
pub symbols: Vec<SymbolInfo<'a>>,
}
impl<'a> CompilationMetadata<'a> {
/// Creates a new empty compilation metadata.
pub fn new() -> Self {
Self {
symbols: Vec::new(),
}
}
/// Adds a symbol to the metadata.
pub fn add_symbol(&mut self, symbol: SymbolInfo<'a>) {
self.symbols.push(symbol);
}
/// Adds a function symbol.
pub fn add_function(
&mut self,
name: Cow<'a, str>,
parameters: Vec<Cow<'a, str>>,
span: Option<Span>,
) {
self.add_function_with_doc(name, parameters, span, None);
}
/// Adds a function symbol with optional doc comment.
pub fn add_function_with_doc(
&mut self,
name: Cow<'a, str>,
parameters: Vec<Cow<'a, str>>,
span: Option<Span>,
description: Option<Cow<'a, str>>,
) {
self.add_symbol(SymbolInfo {
name,
kind: SymbolKind::Function {
parameters,
return_type: None,
},
span,
description,
});
}
/// Adds a syscall symbol.
pub fn add_syscall(
&mut self,
name: Cow<'a, str>,
syscall_type: SyscallType,
argument_count: usize,
span: Option<Span>,
) {
self.add_syscall_with_doc(name, syscall_type, argument_count, span, None);
}
/// Adds a syscall symbol with optional doc comment.
pub fn add_syscall_with_doc(
&mut self,
name: Cow<'a, str>,
syscall_type: SyscallType,
argument_count: usize,
span: Option<Span>,
description: Option<Cow<'a, str>>,
) {
self.add_symbol(SymbolInfo {
name,
kind: SymbolKind::Syscall {
syscall_type,
argument_count,
},
span,
description,
});
}
/// Adds a variable symbol.
pub fn add_variable(&mut self, name: Cow<'a, str>, span: Option<Span>) {
self.add_variable_with_doc(name, span, None);
}
/// Adds a variable symbol with optional doc comment.
pub fn add_variable_with_doc(
&mut self,
name: Cow<'a, str>,
span: Option<Span>,
description: Option<Cow<'a, str>>,
) {
self.add_symbol(SymbolInfo {
name,
kind: SymbolKind::Variable { type_hint: None },
span,
description,
});
}
/// Returns all symbols of a specific kind.
pub fn symbols_of_kind(&self, kind: &str) -> Vec<&SymbolInfo<'a>> {
self.symbols
.iter()
.filter(|sym| match (&sym.kind, kind) {
(SymbolKind::Function { .. }, "function") => true,
(SymbolKind::Syscall { .. }, "syscall") => true,
(SymbolKind::Variable { .. }, "variable") => true,
_ => false,
})
.collect()
}
/// Converts all symbols to LSP SymbolInformation for protocol compatibility.
pub fn to_lsp_symbols(&self, uri: lsp_types::Uri) -> Vec<lsp_types::SymbolInformation> {
self.symbols
.iter()
.map(|sym| sym.to_lsp_symbol_information(uri.clone()))
.collect()
}
/// Converts all symbols to LSP CompletionItems for autocomplete.
pub fn to_lsp_completion_items(&self) -> Vec<lsp_types::CompletionItem> {
self.symbols
.iter()
.map(|sym| sym.to_lsp_completion_item())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metadata_creation() {
let metadata = CompilationMetadata::new();
assert!(metadata.symbols.is_empty());
}
#[test]
fn test_add_function_symbol() {
let mut metadata = CompilationMetadata::new();
metadata.add_function("test_func".into(), vec!["x".into(), "y".into()], None);
assert_eq!(metadata.symbols.len(), 1);
assert_eq!(metadata.symbols[0].name, "test_func");
}
#[test]
fn test_add_syscall_symbol() {
let mut metadata = CompilationMetadata::new();
metadata.add_syscall("hash".into(), SyscallType::System, 1, None);
assert_eq!(metadata.symbols.len(), 1);
assert_eq!(metadata.symbols[0].name, "hash");
}
#[test]
fn test_symbols_of_kind() {
let mut metadata = CompilationMetadata::new();
metadata.add_function("func1".into(), vec![], None);
metadata.add_syscall("hash".into(), SyscallType::System, 1, None);
metadata.add_variable("x".into(), None);
let functions = metadata.symbols_of_kind("function");
assert_eq!(functions.len(), 1);
let syscalls = metadata.symbols_of_kind("syscall");
assert_eq!(syscalls.len(), 1);
let variables = metadata.symbols_of_kind("variable");
assert_eq!(variables.len(), 1);
}
#[test]
fn test_lsp_completion_items() {
let mut metadata = CompilationMetadata::new();
metadata.add_function("test_func".into(), vec![], None);
metadata.add_syscall("hash".into(), SyscallType::System, 1, None);
metadata.add_variable("x".into(), None);
let completions = metadata.to_lsp_completion_items();
assert_eq!(completions.len(), 3);
// Verify function
assert_eq!(completions[0].label, "test_func");
assert_eq!(
completions[0].kind,
Some(lsp_types::CompletionItemKind::FUNCTION)
);
// Verify syscall
assert_eq!(completions[1].label, "hash");
assert_eq!(
completions[1].kind,
Some(lsp_types::CompletionItemKind::FUNCTION)
);
// Verify variable
assert_eq!(completions[2].label, "x");
assert_eq!(
completions[2].kind,
Some(lsp_types::CompletionItemKind::VARIABLE)
);
}
}

View File

@@ -272,3 +272,108 @@ fn device_property_with_underscore_name() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn device_index_read() -> anyhow::Result<()> {
let compiled = compile! {
check "
device printer = \"d0\";
let value = printer[255];
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
get r1 d0 255
move r8 r1
"
}
);
Ok(())
}
#[test]
fn device_index_write() -> anyhow::Result<()> {
let compiled = compile! {
check "
device printer = \"d0\";
printer[255] = 42;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
put d0 255 42
"
}
);
Ok(())
}
#[test]
fn device_index_db_not_allowed() -> anyhow::Result<()> {
let compiled = compile! {
check "
device stack = \"db\";
let x = stack[10];
"
};
assert!(
!compiled.errors.is_empty(),
"Expected error for db indexing"
);
assert!(
compiled.errors[0]
.to_string()
.contains("Direct stack access on 'db' is not yet supported"),
"Expected db restriction error"
);
Ok(())
}
#[test]
fn device_index_db_write_not_allowed() -> anyhow::Result<()> {
let compiled = compile! {
check "
device stack = \"db\";
stack[10] = 42;
"
};
assert!(
!compiled.errors.is_empty(),
"Expected error for db indexing"
);
assert!(
compiled.errors[0]
.to_string()
.contains("Direct stack access on 'db' is not yet supported"),
"Expected db restriction error"
);
Ok(())
}

View File

@@ -47,6 +47,15 @@ macro_rules! compile {
output,
}
}};
(metadata $source:expr) => {{
let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from($source)),
None,
);
let res = compiler.compile();
res.metadata
}};
}
mod binary_expression;
mod branching;
@@ -61,5 +70,6 @@ mod loops;
mod math_syscall;
mod negation_priority;
mod scoping;
mod symbol_documentation;
mod syscall;
mod tuple_literals;

View File

@@ -0,0 +1,120 @@
#[cfg(test)]
mod test {
use anyhow::Result;
#[test]
fn test_variable_doc_comment() -> Result<()> {
let metadata = compile!(metadata "/// this is a documented variable\nlet myVar = 42;");
let var_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "myVar")
.expect("myVar symbol not found");
assert_eq!(
var_symbol.description.as_ref().map(|d| d.as_ref()),
Some("this is a documented variable")
);
Ok(())
}
#[test]
fn test_const_doc_comment() -> Result<()> {
let metadata = compile!(metadata "/// const documentation\nconst myConst = 100;");
let const_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "myConst")
.expect("myConst symbol not found");
assert_eq!(
const_symbol.description.as_ref().map(|d| d.as_ref()),
Some("const documentation")
);
Ok(())
}
#[test]
fn test_device_doc_comment() -> Result<()> {
let metadata = compile!(metadata "/// device documentation\ndevice myDevice = \"d0\";");
let device_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "myDevice")
.expect("myDevice symbol not found");
assert_eq!(
device_symbol.description.as_ref().map(|d| d.as_ref()),
Some("device documentation")
);
Ok(())
}
#[test]
fn test_function_doc_comment() -> Result<()> {
let metadata = compile!(metadata "/// function documentation\nfn test() { }");
let fn_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "test")
.expect("test symbol not found");
assert_eq!(
fn_symbol.description.as_ref().map(|d| d.as_ref()),
Some("function documentation")
);
Ok(())
}
#[test]
fn test_syscall_documentation() -> Result<()> {
let metadata = compile!(metadata "fn test() { clr(d0); }");
let clr_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "clr")
.expect("clr syscall not found");
// clr should have its built-in documentation
assert!(clr_symbol.description.is_some());
assert!(!clr_symbol.description.as_ref().unwrap().is_empty());
Ok(())
}
#[test]
fn test_variable_references_have_tooltips() -> Result<()> {
let metadata = compile!(metadata "/// documented variable\nlet myVar = 5;\nlet x = myVar + 2;\nmyVar = 10;");
// Count how many times 'myVar' appears in symbols
let myvar_symbols: Vec<_> = metadata
.symbols
.iter()
.filter(|s| s.name == "myVar")
.collect();
// We should have at least 2: declaration + 1 reference (in myVar + 2)
// The assignment `myVar = 10` is a write, not a read, so doesn't create a reference
assert!(
myvar_symbols.len() >= 2,
"Expected at least 2 'myVar' symbols (declaration + reference), got {}",
myvar_symbols.len()
);
// All should have the same description
let expected_desc = "documented variable";
for sym in &myvar_symbols {
assert_eq!(
sym.description.as_ref().map(|d| d.as_ref()),
Some(expected_desc),
"Symbol description mismatch at {:?}",
sym.span
);
}
Ok(())
}
}

View File

@@ -287,3 +287,68 @@ fn test_load_reagent() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn test_clr() -> anyhow::Result<()> {
let compiled = compile! {
check
"
device stackDevice = \"d0\";
clr(stackDevice);
let deviceRef = 5;
clr(deviceRef);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
clr d0
move r8 5
clr r8
"
}
);
Ok(())
}
#[test]
fn test_rmap() -> anyhow::Result<()> {
let compiled = compile! {
check
"
device printer = \"d0\";
let reagentHash = 12345;
let itemHash = rmap(printer, reagentHash);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 12345
rmap r15 d0 r8
move r9 r15
"
}
);
Ok(())
}

View File

@@ -8,8 +8,8 @@ use parser::{
tree_node::{
AssignmentExpression, BinaryExpression, BlockExpression, ConstDeclarationExpression,
DeviceDeclarationExpression, Expression, FunctionExpression, IfExpression,
InvocationExpression, Literal, LiteralOr, LiteralOrVariable, LogicalExpression,
LoopExpression, MemberAccessExpression, Spanned, TernaryExpression,
IndexAccessExpression, InvocationExpression, Literal, LiteralOr, LiteralOrVariable,
LogicalExpression, LoopExpression, MemberAccessExpression, Spanned, TernaryExpression,
TupleAssignmentExpression, TupleDeclarationExpression, WhileExpression,
},
};
@@ -67,6 +67,9 @@ pub enum Error<'a> {
#[error("Expected a {0}-tuple, but you're trying to destructure into {1} variables")]
TupleSizeMismatch(usize, usize, Span),
#[error("{0}")]
OperationNotSupported(String, Span),
#[error("{0}")]
Unknown(String, Option<Span>),
}
@@ -89,7 +92,8 @@ impl<'a> From<Error<'a>> for lsp_types::Diagnostic {
| ConstAssignment(_, span)
| DeviceAssignment(_, span)
| AgrumentMismatch(_, span)
| TupleSizeMismatch(_, _, span) => Diagnostic {
| TupleSizeMismatch(_, _, span)
| OperationNotSupported(_, span) => Diagnostic {
range: span.into(),
message: value.to_string(),
severity: Some(DiagnosticSeverity::ERROR),
@@ -141,6 +145,7 @@ struct CompileLocation<'a> {
pub struct CompilationResult<'a> {
pub errors: Vec<Error<'a>>,
pub instructions: Instructions<'a>,
pub metadata: crate::CompilationMetadata<'a>,
}
/// Metadata for the currently compiling function
@@ -198,6 +203,8 @@ pub struct Compiler<'a> {
pub source_map: HashMap<usize, Vec<Span>>,
/// Accumulative errors from the compilation process
pub errors: Vec<Error<'a>>,
/// Metadata about symbols encountered during compilation
pub metadata: crate::CompilationMetadata<'a>,
}
impl<'a> Compiler<'a> {
@@ -215,6 +222,7 @@ impl<'a> Compiler<'a> {
loop_stack: Vec::new(),
source_map: HashMap::new(),
errors: Vec::new(),
metadata: crate::CompilationMetadata::new(),
}
}
@@ -233,6 +241,7 @@ impl<'a> Compiler<'a> {
return CompilationResult {
errors: self.errors,
instructions: self.instructions,
metadata: self.metadata,
};
}
Err(e) => {
@@ -241,6 +250,7 @@ impl<'a> Compiler<'a> {
return CompilationResult {
errors: self.errors,
instructions: self.instructions,
metadata: self.metadata,
};
}
};
@@ -266,6 +276,7 @@ impl<'a> Compiler<'a> {
return CompilationResult {
errors: self.errors,
instructions: self.instructions,
metadata: self.metadata,
};
}
@@ -279,6 +290,7 @@ impl<'a> Compiler<'a> {
CompilationResult {
errors: self.errors,
instructions: self.instructions,
metadata: self.metadata,
}
}
@@ -387,6 +399,26 @@ impl<'a> Compiler<'a> {
}
Expression::Ternary(tern) => Ok(Some(self.expression_ternary(tern.node, scope)?)),
Expression::Invocation(expr_invoke) => {
// Special case: hash() with string literal can be evaluated at compile time
if expr_invoke.node.name.node == "hash" && expr_invoke.node.arguments.len() == 1 {
if let Expression::Literal(Spanned {
node: Literal::String(str_to_hash),
..
}) = &expr_invoke.node.arguments[0].node
{
// Evaluate hash at compile time
let hash_value = crc_hash_signed(str_to_hash);
return Ok(Some(CompileLocation {
location: VariableLocation::Constant(Literal::Number(Number::Integer(
hash_value,
Unit::None,
))),
temp_name: None,
}));
}
}
// Non-constant hash calls or other function calls
self.expression_function_invocation(expr_invoke, scope)?;
// Invocation returns result in r15 (RETURN_REGISTER).
// If used as an expression, we must move it to a temp to avoid overwrite.
@@ -433,10 +465,23 @@ impl<'a> Compiler<'a> {
},
Expression::Variable(name) => {
match scope.get_location_of(&name.node, Some(name.span)) {
Ok(loc) => Ok(Some(CompileLocation {
location: loc,
temp_name: None, // User variable, do not free
})),
Ok(loc) => {
// Track this variable reference in metadata (for tooltips on all usages, not just declaration)
let doc_comment: Option<Cow<'a, str>> = self
.parser
.get_declaration_doc(name.node.as_ref())
.map(|s| Cow::Owned(s) as Cow<'a, str>);
self.metadata.add_variable_with_doc(
name.node.clone(),
Some(name.span),
doc_comment,
);
Ok(Some(CompileLocation {
location: loc,
temp_name: None, // User variable, do not free
}))
}
Err(_) => {
// fallback, check devices
if let Some(device) = self.devices.get(&name.node) {
@@ -487,6 +532,50 @@ impl<'a> Compiler<'a> {
temp_name: Some(result_name),
}))
}
Expression::IndexAccess(access) => {
// "get" behavior (e.g. `let x = d0[255]`)
let IndexAccessExpression { object, index } = access.node;
// 1. Resolve the object to a device string
let (device, dev_cleanup) = self.resolve_device(*object, scope)?;
// Check if device is "db" (not allowed)
if let Operand::Device(ref dev_str) = device {
if dev_str.as_ref() == "db" {
return Err(Error::OperationNotSupported(
"Direct stack access on 'db' is not yet supported".to_string(),
expr.span,
));
}
}
// 2. Compile the index expression to get the address
let (addr, addr_cleanup) = self.compile_operand(*index, scope)?;
// 3. Allocate a temp register for the result
let result_name = self.next_temp_name();
let loc = scope.add_variable(result_name.clone(), LocationRequest::Temp, None)?;
let reg = self.resolve_register(&loc)?;
// 4. Emit get instruction: get rX device address
self.write_instruction(
Instruction::Get(Operand::Register(reg), device, addr),
Some(expr.span),
)?;
// 5. Cleanup
if let Some(c) = dev_cleanup {
scope.free_temp(c, None)?;
}
if let Some(c) = addr_cleanup {
scope.free_temp(c, None)?;
}
Ok(Some(CompileLocation {
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.
@@ -576,6 +665,14 @@ impl<'a> Compiler<'a> {
if let Expression::Variable(ref name) = expr.node
&& let Some(device_id) = self.devices.get(&name.node)
{
// Track this device reference in metadata (for tooltips on all usages, not just declaration)
let doc_comment = self
.parser
.get_declaration_doc(name.node.as_ref())
.map(Cow::Owned);
self.metadata
.add_variable_with_doc(name.node.clone(), Some(expr.span), doc_comment);
return Ok((Operand::Device(device_id.clone()), None));
}
@@ -628,6 +725,14 @@ impl<'a> Compiler<'a> {
let name_str = var_name.node;
let name_span = var_name.span;
// Track the variable in metadata
let doc_comment = self
.parser
.get_declaration_doc(name_str.as_ref())
.map(Cow::Owned);
self.metadata
.add_variable_with_doc(name_str.clone(), Some(name_span), doc_comment);
// optimization. Check for a negated numeric literal (including nested negations)
// e.g., -5, -(-5), -(-(5)), etc.
if let Some(num) = self.try_fold_negation(&expr.node) {
@@ -937,6 +1042,32 @@ impl<'a> Compiler<'a> {
}
(var_loc, None)
}
Expression::IndexAccess(_) => {
// Compile the index access expression
let result = self.expression(expr, scope)?;
let var_loc = scope.add_variable(
name_str.clone(),
LocationRequest::Persist,
Some(name_span),
)?;
if let Some(res) = result {
// Move result from temp to new persistent variable
let result_reg = self.resolve_register(&res.location)?;
self.emit_variable_assignment(&var_loc, Operand::Register(result_reg))?;
// Free the temp result
if let Some(name) = res.temp_name {
scope.free_temp(name, None)?;
}
} else {
return Err(Error::Unknown(
format!("`{name_str}` index access expression did not produce a value"),
Some(name_span),
));
}
(var_loc, None)
}
_ => {
return Err(Error::Unknown(
format!("`{name_str}` declaration of this type is not supported/implemented."),
@@ -961,6 +1092,17 @@ impl<'a> Compiler<'a> {
value: const_value,
} = expr;
// Track the const variable in metadata
let doc_comment = self
.parser
.get_declaration_doc(const_name.node.as_ref())
.map(Cow::Owned);
self.metadata.add_variable_with_doc(
const_name.node.clone(),
Some(const_name.span),
doc_comment,
);
// check for a hash expression or a literal
let value = match const_value {
LiteralOr::Or(Spanned {
@@ -1073,6 +1215,37 @@ impl<'a> Compiler<'a> {
scope.free_temp(c, None)?;
}
}
Expression::IndexAccess(access) => {
// Put instruction: put device address value
let IndexAccessExpression { object, index } = access.node;
let (device, dev_cleanup) = self.resolve_device(*object, scope)?;
// Check if device is "db" (not allowed)
if let Operand::Device(ref dev_str) = device {
if dev_str.as_ref() == "db" {
return Err(Error::OperationNotSupported(
"Direct stack access on 'db' is not yet supported".to_string(),
assignee.span,
));
}
}
let (addr, addr_cleanup) = self.compile_operand(*index, scope)?;
let (val, val_cleanup) = self.compile_operand(*expression, scope)?;
self.write_instruction(Instruction::Put(device, addr, val), Some(assignee.span))?;
if let Some(c) = dev_cleanup {
scope.free_temp(c, None)?;
}
if let Some(c) = addr_cleanup {
scope.free_temp(c, None)?;
}
if let Some(c) = val_cleanup {
scope.free_temp(c, None)?;
}
}
_ => {
return Err(Error::Unknown(
@@ -1353,6 +1526,29 @@ impl<'a> Compiler<'a> {
) -> Result<(), Error<'a>> {
let TupleDeclarationExpression { names, value } = tuple_decl;
// Track each variable in the tuple declaration
// Get doc for the first variable
let first_var_name = names
.iter()
.find(|n| n.node.as_ref() != "_")
.map(|n| n.node.to_string());
let doc_comment = first_var_name
.as_ref()
.and_then(|name| self.parser.get_declaration_doc(name))
.map(Cow::Owned);
for (i, name_spanned) in names.iter().enumerate() {
if name_spanned.node.as_ref() != "_" {
// Only attach doc comment to the first variable
let comment = if i == 0 { doc_comment.clone() } else { None };
self.metadata.add_variable_with_doc(
name_spanned.node.clone(),
Some(name_spanned.span),
comment,
);
}
}
match value.node {
Expression::Invocation(invoke_expr) => {
// Execute the function call - tuple values will be on the stack
@@ -1791,6 +1987,17 @@ impl<'a> Compiler<'a> {
&mut self,
expr: DeviceDeclarationExpression<'a>,
) -> Result<(), Error<'a>> {
// Track the device declaration in metadata
let doc_comment = self
.parser
.get_declaration_doc(expr.name.node.as_ref())
.map(Cow::Owned);
self.metadata.add_variable_with_doc(
expr.name.node.clone(),
Some(expr.name.span),
doc_comment,
);
if self.devices.contains_key(&expr.name.node) {
self.errors.push(Error::DuplicateIdentifier(
expr.name.node.clone(),
@@ -2161,7 +2368,10 @@ impl<'a> Compiler<'a> {
expr: Spanned<BinaryExpression<'a>>,
scope: &mut VariableScope<'a, '_>,
) -> Result<CompileLocation<'a>, Error<'a>> {
fn fold_binary_expression<'a>(expr: &BinaryExpression<'a>) -> Option<Number> {
fn fold_binary_expression<'a>(
expr: &BinaryExpression<'a>,
scope: &VariableScope<'a, '_>,
) -> Option<Number> {
fn number_to_i64(n: Number) -> Option<i64> {
match n {
Number::Integer(i, _) => i64::try_from(i).ok(),
@@ -2190,7 +2400,7 @@ impl<'a> Compiler<'a> {
| BinaryExpression::LeftShift(l, r)
| BinaryExpression::RightShiftArithmetic(l, r)
| BinaryExpression::RightShiftLogical(l, r) => {
(fold_expression(l)?, fold_expression(r)?)
(fold_expression(l, scope)?, fold_expression(r, scope)?)
}
};
@@ -2234,7 +2444,10 @@ impl<'a> Compiler<'a> {
}
}
fn fold_expression<'a>(expr: &Expression<'a>) -> Option<Number> {
fn fold_expression<'a>(
expr: &Expression<'a>,
scope: &VariableScope<'a, '_>,
) -> Option<Number> {
match expr {
// 1. Base Case: It's already a number
Expression::Literal(lit) => match lit.node {
@@ -2243,23 +2456,60 @@ impl<'a> Compiler<'a> {
},
// 2. Handle Parentheses: Just recurse deeper
Expression::Priority(inner) => fold_expression(&inner.node),
Expression::Priority(inner) => fold_expression(&inner.node, scope),
// 3. Handle Negation: Recurse, then negate
Expression::Negation(inner) => {
let val = fold_expression(&inner.node)?;
let val = fold_expression(&inner.node, scope)?;
Some(-val) // Requires impl Neg for Number
}
// 4. Handle Binary Ops: Recurse BOTH sides, then combine
Expression::Binary(bin) => fold_binary_expression(&bin.node),
Expression::Binary(bin) => fold_binary_expression(&bin.node, scope),
// 5. Anything else (Variables, Function Calls) cannot be compile-time folded
// 5. Handle Variable Reference: Check if it's a const
Expression::Variable(var_id) => {
if let Ok(var_loc) = scope.get_location_of(var_id, None) {
if let VariableLocation::Constant(Literal::Number(num)) = var_loc {
return Some(num);
}
}
None
}
// 6. Handle hash() syscall - evaluates to a constant at compile time
Expression::Syscall(Spanned {
node:
SysCall::System(System::Hash(Spanned {
node: Literal::String(str_to_hash),
..
})),
..
}) => {
return Some(Number::Integer(crc_hash_signed(str_to_hash), Unit::None));
}
// 7. Handle hash() macro as invocation - evaluates to a constant at compile time
Expression::Invocation(inv) => {
if inv.node.name.node == "hash" && inv.node.arguments.len() == 1 {
if let Expression::Literal(Spanned {
node: Literal::String(str_to_hash),
..
}) = &inv.node.arguments[0].node
{
// hash() takes a string literal and returns a signed integer
return Some(Number::Integer(crc_hash_signed(str_to_hash), Unit::None));
}
}
None
}
// 8. Anything else cannot be compile-time folded
_ => None,
}
}
if let Some(const_lit) = fold_binary_expression(&expr.node) {
if let Some(const_lit) = fold_binary_expression(&expr.node, scope) {
return Ok(CompileLocation {
location: VariableLocation::Constant(Literal::Number(const_lit)),
temp_name: None,
@@ -2752,6 +3002,17 @@ impl<'a> Compiler<'a> {
span: Span,
scope: &mut VariableScope<'a, '_>,
) -> Result<Option<CompileLocation<'a>>, Error<'a>> {
// Track the syscall in metadata
let syscall_name = expr.name();
let doc = expr.docs().into();
self.metadata.add_syscall_with_doc(
Cow::Borrowed(syscall_name),
crate::SyscallType::System,
expr.arg_count(),
Some(span),
Some(doc),
);
macro_rules! cleanup {
($($to_clean:expr),*) => {
$(
@@ -2773,6 +3034,13 @@ impl<'a> Compiler<'a> {
cleanup!(var_cleanup);
Ok(None)
}
System::Clr(device) => {
let (op, var_cleanup) = self.compile_operand(*device, scope)?;
self.write_instruction(Instruction::Clr(op), Some(span))?;
cleanup!(var_cleanup);
Ok(None)
}
System::Hash(hash_arg) => {
let Spanned {
node: Literal::String(str_lit),
@@ -3092,6 +3360,42 @@ impl<'a> Compiler<'a> {
cleanup!(reagent_cleanup, reagent_hash_cleanup, device_cleanup);
Ok(Some(CompileLocation {
location: VariableLocation::Persistant(VariableScope::RETURN_REGISTER),
temp_name: None,
}))
}
System::Rmap(device, reagent_hash) => {
let Spanned {
node: LiteralOrVariable::Variable(device_spanned),
..
} = device
else {
return Err(Error::AgrumentMismatch(
"Arg1 expected to be a variable".into(),
span,
));
};
let (device, device_cleanup) = self.compile_literal_or_variable(
LiteralOrVariable::Variable(device_spanned),
scope,
)?;
let (reagent_hash, reagent_hash_cleanup) =
self.compile_operand(*reagent_hash, scope)?;
self.write_instruction(
Instruction::Rmap(
Operand::Register(VariableScope::RETURN_REGISTER),
device,
reagent_hash,
),
Some(span),
)?;
cleanup!(reagent_hash_cleanup, device_cleanup);
Ok(Some(CompileLocation {
location: VariableLocation::Persistant(VariableScope::RETURN_REGISTER),
temp_name: None,
@@ -3106,6 +3410,17 @@ impl<'a> Compiler<'a> {
span: Span,
scope: &mut VariableScope<'a, '_>,
) -> Result<Option<CompileLocation<'a>>, Error<'a>> {
// Track the syscall in metadata
let syscall_name = expr.name();
let doc = expr.docs().into();
self.metadata.add_syscall_with_doc(
Cow::Borrowed(syscall_name),
crate::SyscallType::Math,
expr.arg_count(),
Some(span),
Some(doc),
);
macro_rules! cleanup {
($($to_clean:expr),*) => {
$(
@@ -3366,6 +3681,19 @@ impl<'a> Compiler<'a> {
let span = expr.span;
// Track the function definition in metadata
let param_names: Vec<Cow<'a, str>> = arguments.iter().map(|a| a.node.clone()).collect();
let doc_comment = self
.parser
.get_declaration_doc(name.node.as_ref())
.map(Cow::Owned);
self.metadata.add_function_with_doc(
name.node.clone(),
param_names,
Some(name.span),
doc_comment,
);
if self.function_meta.locations.contains_key(&name.node) {
self.errors
.push(Error::DuplicateIdentifier(name.node.clone(), name.span));

View File

@@ -5,12 +5,14 @@ macro_rules! with_syscalls {
// Big names
"yield",
"sleep",
"clr",
"hash",
"load",
"loadBatched",
"loadBatchedNamed",
"loadSlot",
"loadReagent",
"rmap",
"set",
"setBatched",
"setBatchedNamed",

View File

@@ -195,6 +195,9 @@ pub enum Instruction<'a> {
/// `lr register device reagentMode int`
LoadReagent(Operand<'a>, Operand<'a>, Operand<'a>, Operand<'a>),
/// `rmap register device reagentHash` - Resolve Reagent to Item Hash
Rmap(Operand<'a>, Operand<'a>, Operand<'a>),
/// `j label` - Unconditional Jump
Jump(Operand<'a>),
/// `jal label` - Jump and Link (Function Call)
@@ -267,6 +270,8 @@ pub enum Instruction<'a> {
Yield,
/// `sleep val` - Sleep for seconds
Sleep(Operand<'a>),
/// `clr val` - Clear stack memory on device
Clr(Operand<'a>),
/// `alias name target` - Define Alias (Usually handled by compiler, but good for IR)
Alias(Cow<'a, str>, Operand<'a>),
@@ -328,6 +333,9 @@ impl<'a> fmt::Display for Instruction<'a> {
Instruction::LoadReagent(reg, device, reagent_mode, reagent_hash) => {
write!(f, "lr {} {} {} {}", reg, device, reagent_mode, reagent_hash)
}
Instruction::Rmap(reg, device, reagent_hash) => {
write!(f, "rmap {} {} {}", reg, device, reagent_hash)
}
Instruction::Jump(lbl) => write!(f, "j {}", lbl),
Instruction::JumpAndLink(lbl) => write!(f, "jal {}", lbl),
Instruction::JumpRelative(off) => write!(f, "jr {}", off),
@@ -363,6 +371,7 @@ impl<'a> fmt::Display for Instruction<'a> {
}
Instruction::Yield => write!(f, "yield"),
Instruction::Sleep(val) => write!(f, "sleep {}", val),
Instruction::Clr(val) => write!(f, "clr {}", val),
Instruction::Alias(name, target) => write!(f, "alias {} {}", name, target),
Instruction::Define(name, val) => write!(f, "define {} {}", name, val),
Instruction::LabelDef(lbl) => write!(f, "{}:", lbl),

View File

@@ -74,4 +74,39 @@ mod bitwise_tests {
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_sorter_bitwise_operations() {
let source = indoc! {r#"
device self = "db";
device sorter = "d0";
loop {
yield();
// allow Hay with an op_code of `1`
sorter[0] = (hash("ItemCropHay") << 8) | 1;
}
"#};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_bitwise_with_const() {
let source = indoc! {r#"
device sorterOutput = "d0";
const ingotSortClass = 19;
const equals = 0;
loop {
yield();
clr(sorterOutput);
sorterOutput[0] = (ingotSortClass << 16) | (equals << 8) | 3;
}
"#};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
}

View File

@@ -0,0 +1,107 @@
#[cfg(test)]
mod device_indexing_tests {
use crate::common::compile_with_and_without_optimization;
use indoc::indoc;
#[test]
fn test_device_indexing_with_hash_and_binary_literals() {
let source = indoc! {"
device printer = \"d0\";
let item_type = hash(\"ItemIronIngot\");
let quality = 16;
let quantity = 7;
// Pack into a single value using bitwise operations
// Format: (itemHash << 16) | (quality << 8) | quantity
let packed = (item_type << 16) | (quality << 8) | quantity;
// Write to device stack at address 255
let addr = 255;
printer[addr] = packed;
// Read it back
let result = printer[addr];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_device_indexing_with_computed_index() {
let source = indoc! {"
device storage = \"d1\";
let base_addr = 10;
let offset = 5;
let index = base_addr + offset;
let value = 42;
storage[index] = value;
let retrieved = storage[index];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_device_indexing_with_binary_literals() {
let source = indoc! {"
device mem = \"d0\";
// Binary literals for bitwise operations
let flags = 0b1010_0101;
let mask = 0b1111_0000;
let masked = flags & mask;
// Write to device
mem[0] = masked;
// Read back
let read_value = mem[0];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_device_indexing_complex_expression() {
let source = indoc! {"
device db = \"d0\";
let item = hash(\"ItemCopper\");
let quality = 5;
let quantity = 100;
// Complex bitwise expression
let packed = (item << 16) | ((quality & 0xFF) << 8) | (quantity & 0xFF);
// Index with computed address
let slot = 1;
let address = slot * 256 + 100;
db[address] = packed;
// Read back with different computation
let read_addr = (slot + 0) * 256 + 100;
let stored_value = db[read_addr];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_device_indexing_optimization_folds_constants() {
let source = indoc! {"
device cache = \"d0\";
// This should optimize to a single constant
let packed_constant = (hash(\"ItemSilver\") << 16) | (10 << 8) | 50;
cache[255] = packed_constant;
let result = cache[255];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
}

View File

@@ -8,6 +8,8 @@ mod bitwise_tests;
#[cfg(test)]
mod common;
#[cfg(test)]
mod device_indexing_tests;
#[cfg(test)]
mod function_tests;
#[cfg(test)]
mod number_literal_tests;
@@ -65,4 +67,20 @@ mod integration_tests {
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_setbatched_with_member_access() {
let source = indoc! {r#"
const SENSOR = 20088;
const PANELS = hash("StructureSolarPanelDual");
loop {
setBatched(PANELS, "Horizontal", SENSOR.Horizontal);
setBatched(PANELS, "Vertical", SENSOR.Vertical + 90);
yield();
}
"#};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
}

View File

@@ -22,8 +22,11 @@ move r13 r4
move r8 5
move r9 3
and r10 r8 r9
or r11 r8 r9
xor r12 r8 r9
move r1 1
move r10 1
move r2 7
move r11 7
move r3 6
move r12 6
not r4 r8
move r13 r4

View File

@@ -18,9 +18,9 @@ move r11 r3
## Optimized Output
move r8 8
sll r1 r8 2
move r9 r1
sra r2 r8 1
move r10 r2
srl r3 r8 1
move r11 r3
move r1 32
move r9 32
move r2 4
move r10 4
move r3 4
move r11 4

View File

@@ -0,0 +1,21 @@
---
source: libs/integration_tests/src/bitwise_tests.rs
expression: output
---
## Unoptimized Output
j main
main:
__internal_L1:
yield
clr d0
put d0 0 1245187
j __internal_L1
__internal_L2:
## Optimized Output
yield
clr d0
put d0 0 1245187
j 0

View File

@@ -0,0 +1,20 @@
---
source: libs/integration_tests/src/bitwise_tests.rs
assertion_line: 91
expression: output
---
## Unoptimized Output
j main
main:
__internal_L1:
yield
put d0 0 55164456193
j __internal_L1
__internal_L2:
## Optimized Output
yield
put d0 0 55164456193
j 0

View File

@@ -0,0 +1,54 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 90
expression: output
---
## Unoptimized Output
j main
main:
move r8 r15
move r9 5
move r10 100
sll r1 r8 16
and r2 r9 255
sll r3 r2 8
or r4 r1 r3
and r5 r10 255
or r6 r4 r5
move r11 r6
move r12 1
mul r7 r12 256
add r2 r7 100
move r13 r2
put d0 r13 r11
add r1 r12 0
mul r3 r1 256
add r4 r3 100
move r14 r4
get r5 d0 r14
push r5
sub sp sp 1
## Optimized Output
move r8 r15
move r9 5
move r10 100
sll r1 r8 16
move r3 1280
or r4 r1 r3
move r5 100
or r11 r4 r5
move r12 1
move r7 256
move r2 356
move r13 356
put d0 r13 r11
move r1 1
move r3 256
move r4 356
move r14 356
get r5 d0 r14
push r5
sub sp sp 1

View File

@@ -0,0 +1,19 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 105
expression: output
---
## Unoptimized Output
j main
main:
move r8 -62440604628430
put d0 255 r8
get r1 d0 255
move r9 r1
## Optimized Output
move r8 -62440604628430
put d0 255 r8
get r9 d0 255

View File

@@ -0,0 +1,25 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 65
expression: output
---
## Unoptimized Output
j main
main:
move r8 165
move r9 240
and r1 r8 r9
move r10 r1
put d0 0 r10
get r2 d0 0
move r11 r2
## Optimized Output
move r8 165
move r9 240
move r1 160
move r10 160
put d0 0 r10
get r11 d0 0

View File

@@ -0,0 +1,27 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 45
expression: output
---
## Unoptimized Output
j main
main:
move r8 10
move r9 5
add r1 r8 r9
move r10 r1
move r11 42
put d1 r10 r11
get r2 d1 r10
move r12 r2
## Optimized Output
move r8 10
move r9 5
move r1 15
move r10 15
move r11 42
put d1 r10 r11
get r12 d1 r10

View File

@@ -0,0 +1,34 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 27
expression: output
---
## Unoptimized Output
j main
main:
move r8 r15
move r9 16
move r10 7
sll r1 r8 16
sll r2 r9 8
or r3 r1 r2
or r4 r3 r10
move r11 r4
move r12 255
put d0 r12 r11
get r5 d0 r12
move r13 r5
## Optimized Output
move r8 r15
move r9 16
move r10 7
sll r1 r8 16
move r2 4096
or r3 r1 r2
or r11 r3 r10
move r12 255
put d0 r12 r11
get r13 d0 r12

View File

@@ -0,0 +1,27 @@
---
source: libs/integration_tests/src/lib.rs
expression: output
---
## Unoptimized Output
j main
main:
__internal_L1:
l r1 20088 Horizontal
sb -539224550 Horizontal r1
l r2 20088 Vertical
add r3 r2 90
sb -539224550 Vertical r3
yield
j __internal_L1
__internal_L2:
## Optimized Output
l r1 20088 Horizontal
sb -539224550 Horizontal r1
l r2 20088 Vertical
add r3 r2 90
sb -539224550 Vertical r3
yield
j 0

View File

@@ -1,212 +0,0 @@
# Additional Optimization Opportunities for Slang IL Optimizer
## Currently Implemented ✓
1. Constant Propagation - Folds math operations with known values
2. Register Forwarding - Eliminates intermediate moves
3. Function Call Optimization - Removes unnecessary push/pop around calls
4. Leaf Function Optimization - Removes RA save/restore for non-calling functions
5. Redundant Move Elimination - Removes `move rx rx`
6. Dead Code Elimination - Removes unreachable code after jumps
## Proposed Additional Optimizations
### 1. **Algebraic Simplification** 🔥 HIGH IMPACT
Simplify mathematical identities:
- `x + 0``x` (move)
- `x - 0``x` (move)
- `x * 1``x` (move)
- `x * 0``0` (move to constant)
- `x / 1``x` (move)
- `x - x``0` (move to constant)
- `x % 1``0` (move to constant)
**Example:**
```
add r1 r2 0 → move r1 r2
mul r3 r4 1 → move r3 r4
mul r5 r6 0 → move r5 0
```
### 2. **Strength Reduction** 🔥 HIGH IMPACT
Replace expensive operations with cheaper ones:
- `x * 2``add x x x` (addition is cheaper than multiplication)
- `x * power_of_2` → bit shifts (if IC10 supports)
- `x / 2` → bit shifts (if IC10 supports)
**Example:**
```
mul r1 r2 2 → add r1 r2 r2
```
### 3. **Peephole Optimization - Instruction Sequences** 🔥 MEDIUM-HIGH IMPACT
Recognize and optimize common instruction patterns:
#### Pattern: Conditional Branch Simplification
```
seq r1 ra rb → beq ra rb label
beqz r1 label (remove the seq entirely)
sne r1 ra rb → bne ra rb label
beqz r1 label (remove the sne entirely)
```
#### Pattern: Double Move Elimination
```
move r1 r2 → move r1 r3
move r1 r3 (remove first move if r1 not used between)
```
#### Pattern: Redundant Load Elimination
If a register's value is already loaded and hasn't been clobbered:
```
l r1 d0 Temperature
... (no writes to r1)
l r1 d0 Temperature → (remove second load)
```
### 4. **Copy Propagation Enhancement** 🔥 MEDIUM IMPACT
Current register forwarding is good, but we can extend it:
- Track `move` chains: if `r1 = r2` and `r2 = 5`, propagate the `5` directly
- Eliminate the intermediate register if possible
### 5. **Dead Store Elimination** 🔥 MEDIUM IMPACT
Remove writes to registers that are never read before being overwritten:
```
move r1 5
move r1 10 → move r1 10
(first write is dead)
```
### 6. **Common Subexpression Elimination (CSE)** 🔥 MEDIUM-HIGH IMPACT
Recognize when the same computation is done multiple times:
```
add r1 r8 r9
add r2 r8 r9 → add r1 r8 r9
move r2 r1
```
This is especially valuable for expensive operations like:
- Device loads (`l`)
- Math functions (sqrt, sin, cos, etc.)
### 7. **Jump Threading** 🔥 LOW-MEDIUM IMPACT
Optimize jump-to-jump sequences:
```
j label1
...
label1:
j label2 → j label2 (rewrite first jump)
```
### 8. **Branch Folding** 🔥 LOW-MEDIUM IMPACT
Merge consecutive branches to the same target:
```
bgt r1 r2 label
bgt r3 r4 label → Could potentially be optimized based on conditions
```
### 9. **Loop Invariant Code Motion** 🔥 MEDIUM-HIGH IMPACT
Move calculations out of loops if they don't change:
```
loop:
mul r2 5 10 → mul r2 5 10 (hoisted before loop)
add r1 r1 r2 loop:
... add r1 r1 r2
j loop ...
j loop
```
### 10. **Select Instruction Optimization** 🔥 LOW-MEDIUM IMPACT
The `select` instruction can sometimes replace branch patterns:
```
beq r1 r2 else
move r3 r4
j end
else:
move r3 r5 → seq r6 r1 r2
end: select r3 r6 r5 r4
```
### 11. **Stack Access Pattern Optimization** 🔥 LOW IMPACT
If we see repeated `sub r0 sp N` + `get`, we might be able to optimize by:
- Caching the stack address in a register if used multiple times
- Combining sequential gets from adjacent stack slots
### 12. **Inline Small Functions** 🔥 HIGH IMPACT (Complex to implement)
For very small leaf functions (1-2 instructions), inline them at the call site:
```
calculateSum:
add r15 r8 r9
j ra
main:
push 5 → main:
push 10 add r15 5 10
jal calculateSum
```
### 13. **Branch Prediction Hints** 🔥 LOW IMPACT
Reorganize code to put likely branches inline (fall-through) and unlikely branches as jumps.
### 14. **Register Coalescing** 🔥 MEDIUM IMPACT
Reduce register pressure by reusing registers that have non-overlapping lifetimes.
## Priority Implementation Order
### Phase 1 (Quick Wins):
1. Algebraic Simplification (easy, high impact)
2. Strength Reduction (easy, high impact)
3. Dead Store Elimination (medium complexity, good impact)
### Phase 2 (Medium Effort):
4. Peephole Optimizations - seq/beq pattern (medium, high impact)
5. Common Subexpression Elimination (medium, high impact)
6. Copy Propagation Enhancement (medium, medium impact)
### Phase 3 (Advanced):
7. Loop Invariant Code Motion (complex, high impact for loop-heavy code)
8. Function Inlining (complex, high impact)
9. Register Coalescing (complex, medium impact)
## Testing Strategy
- Add test cases for each optimization
- Ensure optimization preserves semantics (run existing tests after each)
- Measure code size reduction
- Consider adding benchmarks to measure game performance impact

View File

@@ -31,6 +31,26 @@ pub fn constant_propagation<'a>(
Instruction::Mod(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| {
if y.is_zero() { Decimal::ZERO } else { x % y }
}),
Instruction::And(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| x & y),
Instruction::Or(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| x | y),
Instruction::Xor(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| x ^ y),
Instruction::Sll(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| {
if y >= 64 { 0 } else { x << y as u32 }
}),
Instruction::Sra(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| {
if y >= 64 {
if x < 0 { -1 } else { 0 }
} else {
x >> y as u32
}
}),
Instruction::Srl(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| {
if y >= 64 {
0
} else {
(x as u64 >> y as u32) as i64
}
}),
Instruction::BranchEq(a, b, l) => {
try_resolve_branch(a, b, l, &registers, |x, y| x == y)
}
@@ -110,6 +130,43 @@ where
))
}
fn decimal_to_i64(d: Decimal) -> i64 {
// Convert decimal to i64, truncating if needed
if let Ok(int_val) = d.try_into() {
int_val
} else {
// For very large or very small values, use a default
if d.is_sign_negative() {
i64::MIN
} else {
i64::MAX
}
}
}
fn i64_to_decimal(i: i64) -> Decimal {
Decimal::from(i)
}
fn try_fold_bitwise<'a, F>(
dst: &Operand<'a>,
a: &Operand<'a>,
b: &Operand<'a>,
regs: &[Option<Decimal>; 16],
op: F,
) -> Option<Instruction<'a>>
where
F: Fn(i64, i64) -> i64,
{
let val_a = resolve_value(a, regs)?;
let val_b = resolve_value(b, regs)?;
let result = op(decimal_to_i64(val_a), decimal_to_i64(val_b));
Some(Instruction::Move(
dst.clone(),
Operand::Number(i64_to_decimal(result)),
))
}
fn try_resolve_branch<'a, F>(
a: &Operand<'a>,
b: &Operand<'a>,

View File

@@ -43,6 +43,7 @@ pub fn get_destination_reg(instr: &Instruction) -> Option<u8> {
| Instruction::Tan(Operand::Register(r), _)
| Instruction::Trunc(Operand::Register(r), _)
| Instruction::LoadReagent(Operand::Register(r), _, _, _)
| Instruction::Rmap(Operand::Register(r), _, _)
| Instruction::Pop(Operand::Register(r)) => Some(*r),
_ => None,
}
@@ -107,6 +108,7 @@ pub fn set_destination_reg<'a>(instr: &Instruction<'a>, new_reg: u8) -> Option<I
Instruction::Sqrt(_, a) => Some(Instruction::Sqrt(r, a.clone())),
Instruction::Tan(_, a) => Some(Instruction::Tan(r, a.clone())),
Instruction::Trunc(_, a) => Some(Instruction::Trunc(r, a.clone())),
Instruction::Rmap(_, a, b) => Some(Instruction::Rmap(r, a.clone(), b.clone())),
_ => None,
}
}
@@ -125,6 +127,9 @@ pub fn reg_is_read(instr: &Instruction, reg: u8) -> bool {
| Instruction::Pow(_, a, b) => check(a) || check(b),
Instruction::Load(_, a, _) => check(a),
Instruction::Store(a, _, b) => check(a) || check(b),
Instruction::StoreBatch(a, _, b) => check(a) || check(b),
Instruction::StoreBatchNamed(a, b, _, c) => check(a) || check(b) || check(c),
Instruction::StoreSlot(a, b, _, c) => check(a) || check(b) || check(c),
Instruction::BranchEq(a, b, _)
| Instruction::BranchNe(a, b, _)
| Instruction::BranchGt(a, b, _)
@@ -133,6 +138,7 @@ pub fn reg_is_read(instr: &Instruction, reg: u8) -> bool {
| Instruction::BranchLe(a, b, _) => check(a) || check(b),
Instruction::BranchEqZero(a, _) | Instruction::BranchNeZero(a, _) => check(a),
Instruction::LoadReagent(_, device, _, item_hash) => check(device) || check(item_hash),
Instruction::Rmap(_, device, reagent_hash) => check(device) || check(reagent_hash),
Instruction::LoadSlot(_, dev, slot, _) => check(dev) || check(slot),
Instruction::LoadBatch(_, dev, _, mode) => check(dev) || check(mode),
Instruction::LoadBatchNamed(_, d_hash, n_hash, _, mode) => {
@@ -167,6 +173,8 @@ pub fn reg_is_read(instr: &Instruction, reg: u8) -> bool {
Instruction::Atan2(_, a, b) | Instruction::Max(_, a, b) | Instruction::Min(_, a, b) => {
check(a) || check(b)
}
Instruction::JumpRelative(a) => check(a),
Instruction::Alias(_, a) => check(a),
_ => false,
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -127,6 +127,52 @@ impl<'a> std::fmt::Display for Math<'a> {
}
}
impl<'a> Math<'a> {
/// Returns the name of this math function (e.g., "acos", "sin", "sqrt", etc.)
pub fn name(&self) -> &'static str {
match self {
Math::Acos(_) => "acos",
Math::Asin(_) => "asin",
Math::Atan(_) => "atan",
Math::Atan2(_, _) => "atan2",
Math::Abs(_) => "abs",
Math::Ceil(_) => "ceil",
Math::Cos(_) => "cos",
Math::Floor(_) => "floor",
Math::Log(_) => "log",
Math::Max(_, _) => "max",
Math::Min(_, _) => "min",
Math::Rand => "rand",
Math::Sin(_) => "sin",
Math::Sqrt(_) => "sqrt",
Math::Tan(_) => "tan",
Math::Trunc(_) => "trunc",
}
}
/// Returns the number of arguments this math function expects
pub fn arg_count(&self) -> usize {
match self {
Math::Acos(_) => 1,
Math::Asin(_) => 1,
Math::Atan(_) => 1,
Math::Atan2(_, _) => 2,
Math::Abs(_) => 1,
Math::Ceil(_) => 1,
Math::Cos(_) => 1,
Math::Floor(_) => 1,
Math::Log(_) => 1,
Math::Max(_, _) => 2,
Math::Min(_, _) => 2,
Math::Rand => 0,
Math::Sin(_) => 1,
Math::Sqrt(_) => 1,
Math::Tan(_) => 1,
Math::Trunc(_) => 1,
}
}
}
documented! {
#[derive(Debug, PartialEq, Eq)]
pub enum System<'a> {
@@ -142,6 +188,12 @@ documented! {
/// ## Slang
/// `sleep(number|var);`
Sleep(Box<Spanned<Expression<'a>>>),
/// Clears stack memory on the provided device.
/// ## IC10
/// `clr d?`
/// ## Slang
/// `clr(device);`
Clr(Box<Spanned<Expression<'a>>>),
/// Gets the in-game hash for a specific prefab name. NOTE! This call is COMPLETELY
/// optimized away unless you bind it to a `let` variable. If you use a `const` variable
/// however, the hash is correctly computed at compile time and substitued automatically.
@@ -249,6 +301,17 @@ documented! {
Spanned<LiteralOrVariable<'a>>,
Spanned<Literal<'a>>,
Box<Spanned<Expression<'a>>>
),
/// Maps a reagent hash to the item hash that fulfills it on a device
///
/// ## IC10
/// `rmap r? d? reagentHash(r?|num)`
/// ## Slang
/// `let itemHash = rmap(device, reagentHash);`
/// `let itemHash = rmap(device, reagentHashValue);`
Rmap(
Spanned<LiteralOrVariable<'a>>,
Box<Spanned<Expression<'a>>>
)
}
}
@@ -258,6 +321,7 @@ impl<'a> std::fmt::Display for System<'a> {
match self {
System::Yield => write!(f, "yield()"),
System::Sleep(a) => write!(f, "sleep({})", a),
System::Clr(a) => write!(f, "clr({})", a),
System::Hash(a) => write!(f, "hash({})", a),
System::LoadFromDevice(a, b) => write!(f, "loadFromDevice({}, {})", a, b),
System::LoadBatch(a, b, c) => write!(f, "loadBatch({}, {}, {})", a, b, c),
@@ -274,6 +338,49 @@ impl<'a> std::fmt::Display for System<'a> {
System::LoadSlot(a, b, c) => write!(f, "loadSlot({}, {}, {})", a, b, c),
System::SetSlot(a, b, c, d) => write!(f, "setSlot({}, {}, {}, {})", a, b, c, d),
System::LoadReagent(a, b, c) => write!(f, "loadReagent({}, {}, {})", a, b, c),
System::Rmap(a, b) => write!(f, "rmap({}, {})", a, b),
}
}
}
impl<'a> System<'a> {
/// Returns the name of this syscall (e.g., "yield", "sleep", "hash", etc.)
pub fn name(&self) -> &'static str {
match self {
System::Yield => "yield",
System::Sleep(_) => "sleep",
System::Clr(_) => "clr",
System::Hash(_) => "hash",
System::LoadFromDevice(_, _) => "loadFromDevice",
System::LoadBatch(_, _, _) => "loadBatch",
System::LoadBatchNamed(_, _, _, _) => "loadBatchNamed",
System::SetOnDevice(_, _, _) => "setOnDevice",
System::SetOnDeviceBatched(_, _, _) => "setOnDeviceBatched",
System::SetOnDeviceBatchedNamed(_, _, _, _) => "setOnDeviceBatchedNamed",
System::LoadSlot(_, _, _) => "loadSlot",
System::SetSlot(_, _, _, _) => "setSlot",
System::LoadReagent(_, _, _) => "loadReagent",
System::Rmap(_, _) => "rmap",
}
}
/// Returns the number of arguments this syscall expects
pub fn arg_count(&self) -> usize {
match self {
System::Yield => 0,
System::Sleep(_) => 1,
System::Clr(_) => 1,
System::Hash(_) => 1,
System::LoadFromDevice(_, _) => 2,
System::LoadBatch(_, _, _) => 3,
System::LoadBatchNamed(_, _, _, _) => 4,
System::SetOnDevice(_, _, _) => 3,
System::SetOnDeviceBatched(_, _, _) => 3,
System::SetOnDeviceBatchedNamed(_, _, _, _) => 4,
System::LoadSlot(_, _, _) => 3,
System::SetSlot(_, _, _, _) => 4,
System::LoadReagent(_, _, _) => 3,
System::Rmap(_, _) => 2,
}
}
}

View File

@@ -149,6 +149,23 @@ fn test_const_hash_expression() -> Result<()> {
Ok(())
}
#[test]
fn test_const_hash() -> Result<()> {
// This test explicitly validates the tokenizer rewind logic.
// When parsing "const h = hash(...)", the parser:
// 1. Consumes "const", identifier, "="
// 2. Attempts to parse "hash(...)" as a literal - this fails
// 3. Must rewind the tokenizer to before "hash"
// 4. Then parse it as a syscall
// If the rewind offset is wrong (e.g., positive instead of negative),
// the tokenizer will be at the wrong position and parsing will fail.
let expr = parser!(r#"const h = hash("ComponentComputer")"#)
.parse()?
.unwrap();
assert_eq!(r#"(const h = hash("ComponentComputer"))"#, expr.to_string());
Ok(())
}
#[test]
fn test_negative_literal_const() -> Result<()> {
let expr = parser!(r#"const i = -123"#).parse()?.unwrap();
@@ -287,3 +304,36 @@ fn test_tuple_declaration_all_complex_expressions() -> Result<()> {
Ok(())
}
#[test]
fn test_eof_error_has_span() -> Result<()> {
// Test that UnexpectedEOF errors capture the span of the last token
let mut parser = parser!("let x = 5");
let result = parser.parse();
// Should have an error
assert!(result.is_err());
let err = result.unwrap_err();
// Check that it's an UnexpectedEOF error
match err {
super::Error::UnexpectedEOF(Some(span)) => {
// Verify the span points to somewhere in the code (not zero defaults)
assert!(
span.start_line > 0 || span.start_col > 0 || span.end_line > 0 || span.end_col > 0,
"Span should not be all zeros: {:?}",
span
);
}
super::Error::UnexpectedEOF(None) => {
eprintln!("ERROR: UnexpectedEOF captured None span instead of previous token span");
eprintln!("This means unexpected_eof() is being called when current_token is None");
panic!("UnexpectedEOF should have captured the previous token's span");
}
other => {
panic!("Expected UnexpectedEOF error, got: {:?}", other);
}
}
Ok(())
}

View File

@@ -209,6 +209,18 @@ impl<'a> std::fmt::Display for MethodCallExpression<'a> {
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct IndexAccessExpression<'a> {
pub object: Box<Spanned<Expression<'a>>>,
pub index: Box<Spanned<Expression<'a>>>,
}
impl<'a> std::fmt::Display for IndexAccessExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}[{}]", self.object, self.index)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum LiteralOrVariable<'a> {
Literal(Literal<'a>),
@@ -402,6 +414,7 @@ pub enum Expression<'a> {
TupleDeclaration(Spanned<TupleDeclarationExpression<'a>>),
Variable(Spanned<Cow<'a, str>>),
While(Spanned<WhileExpression<'a>>),
IndexAccess(Spanned<IndexAccessExpression<'a>>),
}
impl<'a> std::fmt::Display for Expression<'a> {
@@ -450,6 +463,7 @@ impl<'a> std::fmt::Display for Expression<'a> {
Expression::TupleDeclaration(e) => write!(f, "{}", e),
Expression::Variable(id) => write!(f, "{}", id),
Expression::While(e) => write!(f, "{}", e),
Expression::IndexAccess(e) => write!(f, "{}", e),
}
}
}

View File

@@ -68,6 +68,12 @@ impl<'a> Tokenizer<'a> {
Ok(current.map(|t| t.map(|t| self.get_token(t)))?)
}
/// Returns the next token, including comments. Used to preserve doc comments.
pub fn next_token_with_comments(&mut self) -> Result<Option<Token<'a>>, Error> {
let current = self.lexer.next().transpose();
Ok(current.map(|t| t.map(|t| self.get_token(t)))?)
}
}
// ... Iterator and TokenizerBuffer implementations remain unchanged ...
@@ -127,12 +133,28 @@ impl<'a> TokenizerBuffer<'a> {
self.index += 1;
Ok(token)
}
pub fn next_token_with_comments(&mut self) -> Result<Option<Token<'a>>, Error> {
if let Some(token) = self.buffer.pop_front() {
self.history.push_back(token.clone());
self.index += 1;
return Ok(Some(token));
}
let token = self.tokenizer.next_token_with_comments()?;
if let Some(ref token) = token {
self.history.push_back(token.clone());
}
self.index += 1;
Ok(token)
}
pub fn peek(&mut self) -> Result<Option<Token<'a>>, Error> {
if let Some(token) = self.buffer.front() {
return Ok(Some(token.clone()));
}
let Some(new_token) = self.tokenizer.next_token()? else {
let Some(new_token) = self.tokenizer.next_token_with_comments()? else {
return Ok(None);
};
self.buffer.push_front(new_token.clone());
@@ -145,8 +167,20 @@ impl<'a> TokenizerBuffer<'a> {
use Ordering::*;
match seek_to_int.cmp(&0) {
Greater => {
let mut tokens = Vec::with_capacity(seek_to_int as usize);
for _ in 0..seek_to_int {
let mut seek_remaining = seek_to_int as usize;
// First, consume tokens from the buffer (peeked but not yet consumed)
while seek_remaining > 0 && !self.buffer.is_empty() {
if let Some(token) = self.buffer.pop_front() {
self.history.push_back(token);
seek_remaining -= 1;
self.index += 1;
}
}
// Then get tokens from tokenizer if needed
let mut tokens = Vec::with_capacity(seek_remaining);
for _ in 0..seek_remaining {
if let Some(token) = self.tokenizer.next_token()? {
tokens.push(token);
} else {
@@ -157,6 +191,7 @@ impl<'a> TokenizerBuffer<'a> {
}
}
self.history.extend(tokens);
self.index += seek_remaining as i64;
}
Less => {
let seek_to = seek_to_int.unsigned_abs() as usize;

View File

@@ -94,6 +94,30 @@ impl From<lsp_types::Diagnostic> for FfiDiagnostic {
}
}
#[derive_ReprC]
#[repr(C)]
pub struct FfiSymbolKindData {
pub kind: u32, // 0=Function, 1=Syscall, 2=Variable
pub arg_count: u32,
pub syscall_type: u32, // 0=System, 1=Math (only for Syscall kind)
}
#[derive_ReprC]
#[repr(C)]
pub struct FfiSymbolInfo {
pub name: safer_ffi::String,
pub kind_data: FfiSymbolKindData,
pub span: FfiRange,
pub description: safer_ffi::String,
}
#[derive_ReprC]
#[repr(C)]
pub struct FfiDiagnosticsAndSymbols {
pub diagnostics: safer_ffi::Vec<FfiDiagnostic>,
pub symbols: safer_ffi::Vec<FfiSymbolInfo>,
}
#[ffi_export]
pub fn free_ffi_compilation_result(input: FfiCompilationResult) {
drop(input)
@@ -109,6 +133,11 @@ pub fn free_ffi_diagnostic_vec(v: safer_ffi::Vec<FfiDiagnostic>) {
drop(v)
}
#[ffi_export]
pub fn free_ffi_diagnostics_and_symbols(v: FfiDiagnosticsAndSymbols) {
drop(v)
}
#[ffi_export]
pub fn free_string(s: safer_ffi::String) {
drop(s)
@@ -182,6 +211,10 @@ pub fn tokenize_line(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<Ff
let input = String::from_utf16_lossy(input.as_slice());
let tokenizer = Tokenizer::from(input.as_str());
// Build a lookup table for syscall documentation
let syscall_docs: std::collections::HashMap<&'static str, String> =
SysCall::get_all_documentation().into_iter().collect();
let mut tokens = Vec::new();
for token in tokenizer {
@@ -217,13 +250,26 @@ pub fn tokenize_line(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<Ff
}
Ok(Token {
span, token_type, ..
}) => tokens.push(FfiToken {
column: span.start as i32,
error: "".into(),
length: (span.end - span.start) as i32,
tooltip: token_type.docs().into(),
token_kind: token_type.into(),
}),
}) => {
let mut tooltip = token_type.docs();
// If no docs from token type, check if it's a syscall
if tooltip.is_empty() {
if let TokenType::Identifier(id) = &token_type {
if let Some(doc) = syscall_docs.get(id.as_ref()) {
tooltip = doc.clone();
}
}
}
tokens.push(FfiToken {
column: span.start as i32,
error: "".into(),
length: (span.end - span.start) as i32,
tooltip: tooltip.into(),
token_kind: token_type.into(),
})
}
}
}
@@ -257,6 +303,88 @@ pub fn diagnose_source(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<
res.unwrap_or(vec![].into())
}
#[ffi_export]
pub fn diagnose_source_with_symbols(
input: safer_ffi::slice::Ref<'_, u16>,
) -> FfiDiagnosticsAndSymbols {
let res = std::panic::catch_unwind(|| {
let input = String::from_utf16_lossy(input.as_slice());
let tokenizer = Tokenizer::from(input.as_str());
let compiler = Compiler::new(Parser::new(tokenizer), None);
let CompilationResult {
errors: diagnosis,
metadata,
..
} = compiler.compile();
// Convert diagnostics
let mut diagnostics_vec: Vec<FfiDiagnostic> = Vec::with_capacity(diagnosis.len());
for err in diagnosis {
diagnostics_vec.push(lsp_types::Diagnostic::from(err).into());
}
// Convert symbols
let mut symbols_vec: Vec<FfiSymbolInfo> = Vec::with_capacity(metadata.symbols.len());
for symbol in &metadata.symbols {
let (kind, arg_count, syscall_type) = match &symbol.kind {
compiler::SymbolKind::Function { parameters, .. } => {
(0, parameters.len() as u32, 0)
}
compiler::SymbolKind::Syscall {
syscall_type,
argument_count,
} => {
let sc_type = match syscall_type {
compiler::SyscallType::System => 0,
compiler::SyscallType::Math => 1,
};
(1, *argument_count as u32, sc_type)
}
compiler::SymbolKind::Variable { .. } => (2, 0, 0),
};
let span = symbol
.span
.as_ref()
.map(|s| (*s).into())
.unwrap_or(FfiRange {
start_line: 0,
end_line: 0,
start_col: 0,
end_col: 0,
});
symbols_vec.push(FfiSymbolInfo {
name: symbol.name.to_string().into(),
kind_data: FfiSymbolKindData {
kind,
arg_count,
syscall_type,
},
span,
description: symbol
.description
.as_ref()
.map(|d| d.to_string())
.unwrap_or_default()
.into(),
});
}
FfiDiagnosticsAndSymbols {
diagnostics: diagnostics_vec.into(),
symbols: symbols_vec.into(),
}
});
res.unwrap_or(FfiDiagnosticsAndSymbols {
diagnostics: vec![].into(),
symbols: vec![].into(),
})
}
#[ffi_export]
pub fn get_docs() -> safer_ffi::Vec<FfiDocumentedItem> {
let res = std::panic::catch_unwind(|| {

View File

@@ -65,8 +65,8 @@ fn run_logic<'a>() -> Result<(), Error<'a>> {
let input_string = match input_file {
Some(input_path) => {
let mut buf = String::new();
let mut file = std::fs::File::open(input_path).unwrap();
file.read_to_string(&mut buf).unwrap();
let mut file = std::fs::File::open(input_path)?;
file.read_to_string(&mut buf)?;
buf
}
None => {