46 Commits
0.1.1 ... 0.3.1

Author SHA1 Message Date
30b564a153 Merge pull request #28 from dbidwell94/key-not-found-exception
Fixed possible KeyNotFoundException
2025-12-13 01:42:26 -07:00
415e69628d Fixed possible KeyNotFoundException 2025-12-13 01:35:21 -07:00
1755fc3504 Merge pull request #27 from dbidwell94/optimize
Optimize
2025-12-13 00:37:53 -07:00
378c7e18cd version bump 2025-12-13 00:35:31 -07:00
9de59ee3b1 Fix source maps 2025-12-12 21:48:25 -07:00
20f7cb9a4b wip 2025-12-12 17:36:57 -07:00
0be2e644e4 WIP optimization code 2025-12-12 17:23:04 -07:00
3fb04aef3b Emit IL alongside raw IC10 for use in future optimization passes 2025-12-12 15:51:36 -07:00
1230f83951 Merge pull request #26 from dbidwell94/source-map
Source maps
2025-12-11 23:36:02 -07:00
d3974ad590 update changelog and version bump 2025-12-11 23:33:17 -07:00
098d689750 wip -- source mapping overrides in-game line error number 2025-12-11 17:14:43 -07:00
3edf0324c7 populate GlobalCode.sourceMaps 2025-12-11 14:06:54 -07:00
92f0d22805 hook up compilationResult to FFI boundry 2025-12-11 13:32:46 -07:00
811f4f4959 Keep track of source map throughout the compilation process 2025-12-11 13:03:12 -07:00
c041518c9b Merge pull request #25 from dbidwell94/stabalize-functions
0.2.3
2025-12-11 02:27:39 -07:00
2b26d0d278 Update changelog 2025-12-11 02:26:20 -07:00
236b50c813 Allow syscalls in infix operations 2025-12-11 02:24:01 -07:00
342b1ab107 Fix function invocation stack underflow 2025-12-11 01:03:43 -07:00
0732f68bcf Merge pull request #24 from dbidwell94/documentation
QOL
2025-12-10 18:01:21 -07:00
0ac010ef8f Fixed documentation rendering and added ternary expressions 2025-12-10 18:00:20 -07:00
c2208fbb15 Fixed some formatting issues with header markdowns for Stationpedia 2025-12-10 13:39:58 -07:00
295f062797 Merge pull request #23 from dbidwell94/slot-logic
Slot logic
2025-12-10 00:11:45 -07:00
9c260ef2d5 Fixed bug where infix wouldn't rewind when encountering a comma, causing the rest of a syscall parse in an assignment expression to fail 2025-12-10 00:09:16 -07:00
0fde11a2bf Added support for syscalls with assignment expressions 2025-12-09 23:57:19 -07:00
b21d6cc73e Found bug, unable to do an assignment expression with a syscall 2025-12-09 23:45:10 -07:00
f19801d4e6 Merge pull request #22 from dbidwell94/logos
Tech Debt Cleanup
2025-12-09 17:41:02 -07:00
46500a456a Add support for colorized comments 2025-12-09 17:38:35 -07:00
f60c9b32a8 minor version bump 2025-12-09 16:50:23 -07:00
0cddb3e8c8 The Cows are all working. Moo. 2025-12-09 16:31:24 -07:00
c3986ab4d9 VariableManager lifetime errors 2025-12-09 16:05:40 -07:00
f54214acb9 Most of all the errors are gone 2025-12-09 13:59:54 -07:00
d9a7a31306 Lifetimes are declared, now I gotta fix the lifetime issues 2025-12-09 13:51:54 -07:00
d40b759442 TEST -- use Cow instead of String for tokens 2025-12-09 13:17:35 -07:00
080b5320f7 Removed off-by-one calculations in the C# mod 2025-12-09 12:24:29 -07:00
a50a45f0b4 More cleanup 2025-12-09 12:12:28 -07:00
c531f673a5 Remove quickerror in favor of thiserror 2025-12-09 11:32:14 -07:00
23c2ba4134 Fix visual bugs with new span logic 2025-12-09 02:21:56 -07:00
7b7c1f7d29 Logos plugged into Parser 2025-12-09 02:15:43 -07:00
72cf9ea042 wip 2025-12-09 01:43:12 -07:00
fac36c756b Lexer impl done 2025-12-08 23:19:23 -07:00
115a57128c Before error type refactor 2025-12-08 22:50:20 -07:00
6afeec6da2 First pass getting a logos tokenizer up and running 2025-12-08 21:06:42 -07:00
b6123219f8 Merge pull request #21 from dbidwell94/cysharp-removal
Updates for Stationeers Beta branch
2025-12-08 15:26:22 -07:00
f38c15da9c Remove references to Unitask and Cysharp 2025-12-07 23:00:45 -07:00
fdd1a311d5 Merge pull request #20 from dbidwell94/release
Release
2025-12-07 21:46:05 -07:00
fb7ca0d4fd Update About.xml again 2025-12-07 21:45:07 -07:00
49 changed files with 4588 additions and 2355 deletions

51
Changelog.md Normal file
View File

@@ -0,0 +1,51 @@
# Changelog
[0.3.1]
- Fixed possible `KeyNotFoundException` in C# code due to invalid
dictionary access when an IC housing has an error
[0.3.0]
- Implemented a multi-pass optimizer
- This should significantly reduce line count in the final output
- Fixed source map to line up with newly optimized code
[0.2.4]
- Groundwork laid to collect and track source maps
- IC Housing will now display the `Slang` source error line (if available)
instead of the `IC10` source error line
[0.2.3]
- Fixed stack underflow with function invocations
- They are still "heavy", but they should work as expected now
- Fixed issue where syscall functions were not allowed as infix operators
[0.2.2]
- Fixed some formatting issues when converting Markdown to Text Mesh Pro for
Stationpedia
- Added support for ternary expressions
- `let i = someValue ? 4 : 5;`
- `i = someValue ? 4 : 5;`
- This greedily evaluates both sides, so side effects like calling functions
is not recommended i.e.
- `i = someValue : doSomething() : doSomethingElse();`
- Both sides will be evaluated before calling the `select` instruction
[0.2.1]
- Added support for `loadSlot` and `setSlot`
- Fixed bug where syscalls like `max(1, 2)` were not allowed in assignment expressions
[0.2.0]
- Completely re-wrote the tokenizer to use `logos`
- Changed AST and Token data structures to use `Cow` instead of `String`
- Updated error reporting to use `thiserror` instead of `quickerror`
[0.1.2]
- Removed references to `Unitask`

View File

@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>StationeersSlang</Name> <Name>Slang</Name>
<Author>JoeDiertay</Author> <Author>JoeDiertay</Author>
<Version>0.1.1</Version> <Version>0.3.1</Version>
<Description> <Description>
[h1]Slang: High-Level Programming for Stationeers[/h1] [h1]Slang: High-Level Programming for Stationeers[/h1]
Stop writing assembly. Start writing code. Stop writing assembly. Start writing code.
Slang (Stationeers Language) brings modern programming to Stationeers. It allows you to write scripts using a familiar C-style syntax (variables, functions, if/else, loops) directly in the in-game editor. When you hit confirm, Slang compiles your code into optimized IC10 MIPS assembly instantly. Slang (Stationeers Language) brings modern programming to Stationeers. It allows you to write scripts using a familiar C-style syntax (variables, functions, if/else, loops) directly in the in-game editor. When you hit confirm, Slang compiles your code into IC10 instantly.
[b]NOTE: This project is in BETA. Expect updates and changes![/b] [b]NOTE: This project is in BETA. Expect updates and changes![/b]
@@ -91,7 +91,7 @@ A: Yes! Slang does not modify any existing IC10 code, it is only a compiler. As
<OrderBefore WorkshopHandle="3592775931" /> <OrderBefore WorkshopHandle="3592775931" />
<InGameDescription><![CDATA[ <InGameDescription><![CDATA[
<size=30><color=#ffff00>Slang - High Level Language Compiler</color></size> <size=30><color=#ffff00>Slang - High Level Language Compiler</color></size>
A modern programming experience for Stationeers. Write C-style code that compiles to MIPS assembly instantly. A modern programming experience for Stationeers. Write C-style code that compiles to IC10 instantly.
<color=#ffa500><b>Features</b></color> <color=#ffa500><b>Features</b></color>
- <b>In-Game Compilation:</b> Write high-level logic directly in the chip editor. - <b>In-Game Compilation:</b> Write high-level logic directly in the chip editor.

View File

@@ -55,7 +55,7 @@ public static unsafe class SlangExtensions
var color = GetColorForKind(token.token_kind); var color = GetColorForKind(token.token_kind);
int colIndex = token.column - 1; int colIndex = token.column;
if (colIndex < 0) if (colIndex < 0)
colIndex = 0; colIndex = 0;
@@ -100,10 +100,10 @@ public static unsafe class SlangExtensions
Severity = item.severity, Severity = item.severity,
Range = new Slang.Range Range = new Slang.Range
{ {
EndCol = Math.Max(item.range.end_col - 2, 0), EndCol = Math.Max(item.range.end_col, 0),
EndLine = item.range.end_line - 1, EndLine = item.range.end_line,
StartCol = Math.Max(item.range.start_col - 2, 0), StartCol = Math.Max(item.range.start_col, 0),
StartLine = item.range.end_line - 1, StartLine = item.range.start_line,
}, },
} }
); );
@@ -113,6 +113,34 @@ public static unsafe class SlangExtensions
return toReturn; return toReturn;
} }
public static unsafe List<SourceMapEntry> ToList(this Vec_FfiSourceMapEntry_t vec)
{
var toReturn = new List<SourceMapEntry>((int)vec.len);
var currentPtr = vec.ptr;
for (int i = 0; i < (int)vec.len; i++)
{
var item = currentPtr[i];
toReturn.Add(
new SourceMapEntry
{
Ic10Line = item.line_number,
SlangSource = new Range
{
EndCol = item.span.end_col,
EndLine = item.span.end_line,
StartCol = item.span.start_col,
StartLine = item.span.start_line,
},
}
);
}
return toReturn;
}
private static uint GetColorForKind(uint kind) private static uint GetColorForKind(uint kind)
{ {
switch (kind) switch (kind)
@@ -134,6 +162,9 @@ public static unsafe class SlangExtensions
case 7: // (punctuation) case 7: // (punctuation)
return SlangFormatter.ColorDefault; return SlangFormatter.ColorDefault;
case 8: // Comments
return SlangFormatter.ColorComment;
case 10: // (syscalls) case 10: // (syscalls)
return SlangFormatter.ColorFunction; return SlangFormatter.ColorFunction;

View File

@@ -71,18 +71,6 @@ public unsafe struct Vec_uint8_t {
public UIntPtr cap; public UIntPtr cap;
} }
public unsafe partial class Ffi {
/// <summary>
/// C# handles strings as UTF16. We do NOT want to allocate that memory in C# because
/// we want to avoid GC. So we pass it to Rust to handle all the memory allocations.
/// This should result in the ability to compile many times without triggering frame drops
/// from the GC from a <c>GetBytes()</c> call on a string in C#.
/// </summary>
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
Vec_uint8_t compile_from_string (
slice_ref_uint16_t input);
}
[StructLayout(LayoutKind.Sequential, Size = 16)] [StructLayout(LayoutKind.Sequential, Size = 16)]
public unsafe struct FfiRange_t { public unsafe struct FfiRange_t {
public UInt32 start_col; public UInt32 start_col;
@@ -94,6 +82,44 @@ public unsafe struct FfiRange_t {
public UInt32 end_line; public UInt32 end_line;
} }
[StructLayout(LayoutKind.Sequential, Size = 20)]
public unsafe struct FfiSourceMapEntry_t {
public UInt32 line_number;
public FfiRange_t span;
}
/// <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_FfiSourceMapEntry_t {
public FfiSourceMapEntry_t * ptr;
public UIntPtr len;
public UIntPtr cap;
}
[StructLayout(LayoutKind.Sequential, Size = 48)]
public unsafe struct FfiCompilationResult_t {
public Vec_uint8_t output_code;
public Vec_FfiSourceMapEntry_t source_map;
}
public unsafe partial class Ffi {
/// <summary>
/// C# handles strings as UTF16. We do NOT want to allocate that memory in C# because
/// we want to avoid GC. So we pass it to Rust to handle all the memory allocations.
/// This should result in the ability to compile many times without triggering frame drops
/// from the GC from a <c>GetBytes()</c> call on a string in C#.
/// </summary>
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
FfiCompilationResult_t compile_from_string (
slice_ref_uint16_t input);
}
[StructLayout(LayoutKind.Sequential, Size = 48)] [StructLayout(LayoutKind.Sequential, Size = 48)]
public unsafe struct FfiDiagnostic_t { public unsafe struct FfiDiagnostic_t {
public Vec_uint8_t message; public Vec_uint8_t message;
@@ -146,6 +172,12 @@ public unsafe partial class Ffi {
Vec_FfiDocumentedItem_t v); Vec_FfiDocumentedItem_t v);
} }
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
void free_ffi_compilation_result (
FfiCompilationResult_t input);
}
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 (

View File

@@ -4,7 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Cysharp.Threading.Tasks; using System.Threading.Tasks;
using StationeersIC10Editor; using StationeersIC10Editor;
public class SlangFormatter : ICodeFormatter public class SlangFormatter : ICodeFormatter
@@ -98,29 +98,32 @@ public class SlangFormatter : ICodeFormatter
inputSrc = this.RawText; inputSrc = this.RawText;
} }
HandleLsp(inputSrc, token).Forget(); _ = HandleLsp(inputSrc, token);
} }
private async UniTaskVoid HandleLsp(string inputSrc, CancellationToken cancellationToken) private async Task HandleLsp(string inputSrc, CancellationToken cancellationToken)
{ {
try try
{ {
await UniTask.SwitchToThreadPool(); if (cancellationToken.IsCancellationRequested)
return;
await Task.Delay(200, cancellationToken: cancellationToken);
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
return; return;
await System.Threading.Tasks.Task.Delay(200, cancellationToken: cancellationToken); // Running this potentially CPU intensive work on a background thread.
var dict = await Task.Run(
if (cancellationToken.IsCancellationRequested) () =>
return; {
return Marshal
var dict = Marshal .DiagnoseSource(inputSrc)
.DiagnoseSource(inputSrc) .GroupBy(d => d.Range.StartLine)
.GroupBy(d => d.Range.StartLine) .ToDictionary(g => g.Key);
.ToDictionary(g => g.Key); },
cancellationToken
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken); );
ApplyDiagnostics(dict); ApplyDiagnostics(dict);
} }
@@ -136,7 +139,6 @@ public class SlangFormatter : ICodeFormatter
{ {
HashSet<uint> linesToRefresh; HashSet<uint> linesToRefresh;
// CRITICAL FIX FOR LINE SHIFTS:
// If the line count has changed (lines added/deleted), indices have shifted. // If the line count has changed (lines added/deleted), indices have shifted.
// We must refresh ALL lines to ensure any line that shifted into a new position // We must refresh ALL lines to ensure any line that shifted into a new position
// gets scrubbed of its old visual state. // gets scrubbed of its old visual state.

View File

@@ -15,11 +15,64 @@ public static class GlobalCode
// so that save file data is smaller // so that save file data is smaller
private static Dictionary<Guid, string> codeDict = new(); private static Dictionary<Guid, string> codeDict = new();
// This Dictionary stores the source maps for the given SLANG_REF, where
// the key is the IC10 line, and the value is a List of Slang ranges where that
// line would have come from
private static Dictionary<Guid, Dictionary<uint, List<Range>>> sourceMaps = new();
public static void ClearCache() public static void ClearCache()
{ {
codeDict.Clear(); codeDict.Clear();
} }
public static void SetSourceMap(Guid reference, List<SourceMapEntry> sourceMapEntries)
{
var builtDictionary = new Dictionary<uint, List<Range>>();
foreach (var entry in sourceMapEntries)
{
if (!builtDictionary.ContainsKey(entry.Ic10Line))
{
builtDictionary[entry.Ic10Line] = new();
}
builtDictionary[entry.Ic10Line].Add(entry.SlangSource);
}
sourceMaps[reference] = builtDictionary;
}
public static bool GetSlangErrorLineFromICError(
Guid reference,
uint icErrorLine,
out uint slangSrc,
out Range slangSpan
)
{
slangSrc = icErrorLine;
slangSpan = new Range { };
if (!sourceMaps.ContainsKey(reference))
{
return false;
}
if (!sourceMaps[reference].ContainsKey(icErrorLine))
{
return false;
}
var foundRange = sourceMaps[reference][icErrorLine];
if (foundRange is null)
{
return false;
}
slangSrc = foundRange[0].StartLine;
slangSpan = foundRange[0];
return true;
}
public static string GetSource(Guid reference) public static string GetSource(Guid reference)
{ {
if (!codeDict.ContainsKey(reference)) if (!codeDict.ContainsKey(reference))

View File

@@ -10,10 +10,23 @@ using StationeersIC10Editor;
public struct Range public struct Range
{ {
public uint StartCol; public uint StartCol = 0;
public uint EndCol; public uint EndCol = 0;
public uint StartLine; public uint StartLine = 0;
public uint EndLine; public uint EndLine = 0;
public Range(uint startLine, uint startCol, uint endLine, uint endCol)
{
StartLine = startLine;
StartCol = startCol;
EndLine = endLine;
EndCol = endCol;
}
public override string ToString()
{
return $"L{StartLine}C{StartCol} - L{EndLine}C{EndCol}";
}
} }
public struct Diagnostic public struct Diagnostic
@@ -23,6 +36,17 @@ public struct Diagnostic
public Range Range; public Range Range;
} }
public struct SourceMapEntry
{
public Range SlangSource;
public uint Ic10Line;
public override string ToString()
{
return $"IC10: {Ic10Line} Slang: `{SlangSource}`";
}
}
public static class Marshal public static class Marshal
{ {
private static IntPtr _libraryHandle = IntPtr.Zero; private static IntPtr _libraryHandle = IntPtr.Zero;
@@ -78,11 +102,16 @@ public static class Marshal
} }
} }
public static unsafe bool CompileFromString(string inputString, out string compiledString) public static unsafe bool CompileFromString(
string inputString,
out string compiledString,
out List<SourceMapEntry> sourceMapEntries
)
{ {
if (String.IsNullOrEmpty(inputString) || !EnsureLibLoaded()) if (String.IsNullOrEmpty(inputString) || !EnsureLibLoaded())
{ {
compiledString = String.Empty; compiledString = String.Empty;
sourceMapEntries = new();
return false; return false;
} }
@@ -95,19 +124,16 @@ public static class Marshal
}; };
var result = Ffi.compile_from_string(input); var result = Ffi.compile_from_string(input);
try try
{ {
if ((ulong)result.len < 1) sourceMapEntries = result.source_map.ToList();
{ compiledString = result.output_code.AsString();
compiledString = String.Empty;
return false;
}
compiledString = result.AsString();
return true; return true;
} }
finally finally
{ {
result.Drop(); Ffi.free_ffi_compilation_result(result);
} }
} }
} }

View File

@@ -1,17 +1,43 @@
namespace Slang; namespace Slang;
using System; using System;
using System.Runtime.CompilerServices;
using Assets.Scripts.Objects; using Assets.Scripts.Objects;
using Assets.Scripts.Objects.Electrical; using Assets.Scripts.Objects.Electrical;
using Assets.Scripts.Objects.Motherboards; using Assets.Scripts.Objects.Motherboards;
using Assets.Scripts.UI; using Assets.Scripts.UI;
using HarmonyLib; using HarmonyLib;
class LineErrorData
{
public AsciiString SourceRef;
public uint IC10ErrorSource;
public string SlangErrorReference;
public Range SlangErrorSpan;
public LineErrorData(
AsciiString sourceRef,
uint ic10ErrorSource,
string slangErrorRef,
Range slangErrorSpan
)
{
this.SourceRef = sourceRef;
this.IC10ErrorSource = ic10ErrorSource;
this.SlangErrorReference = slangErrorRef;
this.SlangErrorSpan = slangErrorSpan;
}
}
[HarmonyPatch] [HarmonyPatch]
public static class SlangPatches public static class SlangPatches
{ {
private static ProgrammableChipMotherboard? _currentlyEditingMotherboard; private static ProgrammableChipMotherboard? _currentlyEditingMotherboard;
private static AsciiString? _motherboardCachedCode; private static AsciiString? _motherboardCachedCode;
private static Guid? _currentlyEditingGuid;
private static ConditionalWeakTable<ProgrammableChip, LineErrorData> _errorReferenceTable =
new();
[HarmonyPatch( [HarmonyPatch(
typeof(ProgrammableChipMotherboard), typeof(ProgrammableChipMotherboard),
@@ -25,17 +51,20 @@ public static class SlangPatches
// guard to ensure we have valid IC10 before continuing // guard to ensure we have valid IC10 before continuing
if ( if (
!SlangPlugin.IsSlangSource(ref result) !SlangPlugin.IsSlangSource(ref result)
|| !Marshal.CompileFromString(result, out string compiled) || !Marshal.CompileFromString(result, out var compiled, out var sourceMap)
|| string.IsNullOrEmpty(compiled) || string.IsNullOrEmpty(compiled)
) )
{ {
return; return;
} }
var thisRef = Guid.NewGuid(); var thisRef = _currentlyEditingGuid ?? Guid.NewGuid();
// Ensure we cache this compiled code for later retreival. // Ensure we cache this compiled code for later retreival.
GlobalCode.SetSource(thisRef, result); GlobalCode.SetSource(thisRef, result);
GlobalCode.SetSourceMap(thisRef, sourceMap);
_currentlyEditingGuid = null;
// Append REF to the bottom // Append REF to the bottom
compiled += $"\n{GlobalCode.SLANG_REF}{thisRef}"; compiled += $"\n{GlobalCode.SLANG_REF}{thisRef}";
@@ -77,6 +106,7 @@ public static class SlangPatches
return; return;
} }
_currentlyEditingGuid = sourceRef;
var slangSource = GlobalCode.GetSource(sourceRef); var slangSource = GlobalCode.GetSource(sourceRef);
if (string.IsNullOrEmpty(slangSource)) if (string.IsNullOrEmpty(slangSource))
@@ -136,6 +166,100 @@ public static class SlangPatches
chipData.SourceCode = code; chipData.SourceCode = code;
} }
[HarmonyPatch(
typeof(ProgrammableChip),
nameof(ProgrammableChip.ErrorLineNumberString),
MethodType.Getter
)]
[HarmonyPostfix]
public static void pgc_ErrorLineNumberString(ProgrammableChip __instance, ref string __result)
{
if (
String.IsNullOrEmpty(__result)
|| !uint.TryParse(__result.Trim(), out var ic10ErrorLineNumber)
)
{
return;
}
var sourceAscii = __instance.GetSourceCode();
if (_errorReferenceTable.TryGetValue(__instance, out var cache))
{
if (cache.SourceRef.Equals(sourceAscii) && cache.IC10ErrorSource == ic10ErrorLineNumber)
{
__result = cache.SlangErrorReference;
return;
}
}
var source = System.Text.Encoding.UTF8.GetString(
System.Text.Encoding.ASCII.GetBytes(__instance.GetSourceCode())
);
var slangIndex = source.LastIndexOf(GlobalCode.SLANG_REF);
if (
slangIndex < 0
|| !Guid.TryParse(
source
.Substring(
source.LastIndexOf(GlobalCode.SLANG_REF) + GlobalCode.SLANG_REF.Length
)
.Trim(),
out var slangGuid
)
|| !GlobalCode.GetSlangErrorLineFromICError(
slangGuid,
ic10ErrorLineNumber,
out var slangErrorLineNumber,
out var slangSpan
)
)
{
return;
}
L.Warning($"IC error at: {__result} -- Slang source error line: {slangErrorLineNumber}");
__result = slangErrorLineNumber.ToString();
_errorReferenceTable.Remove(__instance);
_errorReferenceTable.Add(
__instance,
new LineErrorData(
sourceAscii,
ic10ErrorLineNumber,
slangErrorLineNumber.ToString(),
slangSpan
)
);
}
[HarmonyPatch(
typeof(ProgrammableChip),
nameof(ProgrammableChip.SetSourceCode),
new Type[] { typeof(string) }
)]
[HarmonyPostfix]
public static void pgc_SetSourceCode_string(ProgrammableChip __instance, string sourceCode)
{
_errorReferenceTable.Remove(__instance);
}
[HarmonyPatch(
typeof(ProgrammableChip),
nameof(ProgrammableChip.SetSourceCode),
new Type[] { typeof(string), typeof(ICircuitHolder) }
)]
[HarmonyPostfix]
public static void pgc_SetSourceCode_string_parent(
ProgrammableChip __instance,
string sourceCode,
ICircuitHolder parent
)
{
_errorReferenceTable.Remove(__instance);
}
[HarmonyPatch( [HarmonyPatch(
typeof(ProgrammableChipMotherboard), typeof(ProgrammableChipMotherboard),
nameof(ProgrammableChipMotherboard.SerializeSave) nameof(ProgrammableChipMotherboard.SerializeSave)
@@ -223,6 +347,7 @@ public static class SlangPatches
_currentlyEditingMotherboard = null; _currentlyEditingMotherboard = null;
_motherboardCachedCode = null; _motherboardCachedCode = null;
_currentlyEditingGuid = null;
} }
[HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))] [HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))]

View File

@@ -41,7 +41,7 @@ namespace Slang
{ {
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.1"; public const string PluginVersion = "0.3.1";
public static Mod MOD = new Mod(PluginName, PluginVersion); public static Mod MOD = new Mod(PluginName, PluginVersion);

View File

@@ -26,12 +26,18 @@ public static class TextMeshProFormatter
RegexOptions.Singleline RegexOptions.Singleline
); );
// 3. Handle Headers (## Header)
// Convert ## Header to large bold text
text = Regex.Replace( text = Regex.Replace(
text, text,
@"^##(\s+)?(.+)$", @"^\s*##\s+(.+)$",
"<size=120%><b>$1</b></size>", "<size=110%><color=#ffffff><b>$1</b></color></size>",
RegexOptions.Multiline
);
// 3. Handle # Headers SECOND (General)
text = Regex.Replace(
text,
@"^\s*#\s+(.+)$",
"<size=120%><color=#ffffff><b>$1</b></color></size>",
RegexOptions.Multiline RegexOptions.Multiline
); );

View File

@@ -5,7 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>StationeersSlang</AssemblyName> <AssemblyName>StationeersSlang</AssemblyName>
<Description>Slang Compiler Bridge</Description> <Description>Slang Compiler Bridge</Description>
<Version>0.1.1</Version> <Version>0.3.1</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
@@ -39,10 +39,6 @@
<HintPath>./ref/Assembly-CSharp.dll</HintPath> <HintPath>./ref/Assembly-CSharp.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="UniTask">
<HintPath>./ref/UniTask.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="IC10Editor.dll"> <Reference Include="IC10Editor.dll">
<HintPath>./ref/IC10Editor.dll</HintPath> <HintPath>./ref/IC10Editor.dll</HintPath>

131
rust_compiler/Cargo.lock generated
View File

@@ -28,6 +28,15 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.21" version = "0.6.21"
@@ -114,6 +123,12 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "beef"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -253,12 +268,13 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"helpers", "helpers",
"il",
"indoc", "indoc",
"lsp-types", "lsp-types",
"parser", "parser",
"pretty_assertions", "pretty_assertions",
"quick-error",
"rust_decimal", "rust_decimal",
"thiserror",
"tokenizer", "tokenizer",
] ]
@@ -327,6 +343,12 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "funty" name = "funty"
version = "2.0.0" version = "2.0.0"
@@ -376,6 +398,15 @@ name = "helpers"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"lsp-types",
]
[[package]]
name = "il"
version = "0.1.0"
dependencies = [
"helpers",
"rust_decimal",
] ]
[[package]] [[package]]
@@ -434,6 +465,40 @@ 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 = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "logos"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a790d11254054e5dc83902dba85d253ff06ceb0cfafb12be8773435cb9dfb4f4"
dependencies = [
"logos-derive",
]
[[package]]
name = "logos-codegen"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60337c43a38313b58871f8d5d76872b8e17aa9d51fad494b5e76092c0ce05f5"
dependencies = [
"beef",
"fnv",
"proc-macro2",
"quote",
"regex-automata",
"regex-syntax",
"rustc_version",
"syn 2.0.111",
]
[[package]]
name = "logos-derive"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d151b2ae667f69e10b8738f5cac0c746faa22b2e15ea7e83b55476afec3767dc"
dependencies = [
"logos-codegen",
]
[[package]] [[package]]
name = "lsp-types" name = "lsp-types"
version = "0.97.0" version = "0.97.0"
@@ -508,6 +573,16 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "optimizer"
version = "0.1.0"
dependencies = [
"anyhow",
"helpers",
"il",
"rust_decimal",
]
[[package]] [[package]]
name = "parser" name = "parser"
version = "0.1.0" version = "0.1.0"
@@ -516,7 +591,8 @@ dependencies = [
"helpers", "helpers",
"lsp-types", "lsp-types",
"pretty_assertions", "pretty_assertions",
"quick-error", "safer-ffi",
"thiserror",
"tokenizer", "tokenizer",
] ]
@@ -593,12 +669,6 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.42" version = "1.0.42"
@@ -644,6 +714,23 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]] [[package]]
name = "rend" name = "rend"
version = "0.4.2" version = "0.4.2"
@@ -843,17 +930,18 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]] [[package]]
name = "slang" name = "slang"
version = "0.1.1" version = "0.3.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"compiler", "compiler",
"helpers", "helpers",
"lsp-types", "lsp-types",
"optimizer",
"parser", "parser",
"quick-error",
"rust_decimal", "rust_decimal",
"safer-ffi", "safer-ffi",
"thiserror",
"tokenizer", "tokenizer",
] ]
@@ -926,6 +1014,26 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.10.0" version = "1.10.0"
@@ -947,9 +1055,10 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"helpers", "helpers",
"logos",
"lsp-types", "lsp-types",
"quick-error",
"rust_decimal", "rust_decimal",
"thiserror",
] ]
[[package]] [[package]]

View File

@@ -1,17 +1,18 @@
[package] [package]
name = "slang" name = "slang"
version = "0.1.1" version = "0.3.1"
edition = "2021" edition = "2021"
[workspace] [workspace]
members = ["libs/*"] members = ["libs/*"]
[workspace.dependencies] [workspace.dependencies]
quick-error = "2" thiserror = "2"
rust_decimal = "1" rust_decimal = "1"
safer-ffi = { version = "0.1" } # Safely share structs in memory between C# and Rust safer-ffi = { version = "0.1" } # Safely share structs in memory between C# and Rust
lsp-types = { version = "0.97" } # Allows for LSP style reporting to the frontend lsp-types = { version = "0.97" } # Allows for LSP style reporting to the frontend
crc32fast = "1.5" # This is for `HASH(..)` calls to be optimized away crc32fast = "1.5" # This is for `HASH(..)` calls to be optimized away
anyhow = { version = "^1.0", features = ["backtrace"] }
[features] [features]
headers = ["safer-ffi/headers"] headers = ["safer-ffi/headers"]
@@ -36,13 +37,14 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
clap = { version = "^4.5", features = ["derive"] } clap = { version = "^4.5", features = ["derive"] }
lsp-types = { workspace = true } lsp-types = { workspace = true }
quick-error = { workspace = true } thiserror = { workspace = true }
rust_decimal = { workspace = true } 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" } helpers = { path = "libs/helpers" }
optimizer = { path = "libs/optimizer" }
safer-ffi = { workspace = true } safer-ffi = { workspace = true }
anyhow = { workspace = true }
[dev-dependencies] [dev-dependencies]
anyhow = { version = "^1.0", features = ["backtrace"] }

View File

@@ -4,10 +4,11 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
quick-error = { workspace = true } thiserror = { workspace = true }
parser = { path = "../parser" } parser = { path = "../parser" }
tokenizer = { path = "../tokenizer" } tokenizer = { path = "../tokenizer" }
helpers = { path = "../helpers" } helpers = { path = "../helpers" }
il = { path = "../il" }
lsp-types = { workspace = true } lsp-types = { workspace = true }
rust_decimal = { workspace = true } rust_decimal = { workspace = true }

View File

@@ -3,4 +3,4 @@ mod test;
mod v1; mod v1;
mod variable_manager; mod variable_manager;
pub use v1::{Compiler, CompilerConfig, Error}; pub use v1::{CompilationResult, Compiler, CompilerConfig, Error};

View File

@@ -1,9 +1,10 @@
use crate::compile; use crate::compile;
use anyhow::Result;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn simple_binary_expression() -> anyhow::Result<()> { fn simple_binary_expression() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug debug
" "
@@ -17,7 +18,7 @@ fn simple_binary_expression() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 3 #i move r8 3
" "
} }
); );
@@ -26,7 +27,7 @@ fn simple_binary_expression() -> anyhow::Result<()> {
} }
#[test] #[test]
fn nested_binary_expressions() -> anyhow::Result<()> { fn nested_binary_expressions() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug debug
" "
@@ -44,13 +45,15 @@ fn nested_binary_expressions() -> anyhow::Result<()> {
" "
j main j main
calculateArgs: calculateArgs:
pop r8 #arg3 pop r8
pop r9 #arg2 pop r9
pop r10 #arg1 pop r10
push ra push ra
add r1 r10 r9 add r1 r10 r9
mul r2 r1 r8 mul r2 r1 r8
move r15 r2 move r15 r2
j L1
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 1 sub sp sp 1
@@ -60,9 +63,9 @@ fn nested_binary_expressions() -> anyhow::Result<()> {
push 20 push 20
push 30 push 30
jal calculateArgs jal calculateArgs
move r1 r15 #__binary_temp_3 move r1 r15
add r2 r1 100 add r2 r1 100
move r8 r2 #returned move r8 r2
" "
} }
); );
@@ -71,7 +74,7 @@ fn nested_binary_expressions() -> anyhow::Result<()> {
} }
#[test] #[test]
fn stress_test_constant_folding() -> anyhow::Result<()> { fn stress_test_constant_folding() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug debug
" "
@@ -85,7 +88,7 @@ fn stress_test_constant_folding() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 -123 #negationHell move r8 -123
" "
} }
); );
@@ -94,7 +97,7 @@ fn stress_test_constant_folding() -> anyhow::Result<()> {
} }
#[test] #[test]
fn test_constant_folding_with_variables_mixed_in() -> anyhow::Result<()> { fn test_constant_folding_with_variables_mixed_in() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug debug
r#" r#"
@@ -113,7 +116,59 @@ fn test_constant_folding_with_variables_mixed_in() -> anyhow::Result<()> {
mul r2 373.2 r1 mul r2 373.2 r1
sub r3 1 r2 sub r3 1 r2
add r4 r3 518.15 add r4 r3 518.15
move r8 r4 #i move r8 r4
"
}
);
Ok(())
}
#[test]
fn test_ternary_expression() -> Result<()> {
let compiled = compile! {
debug
r#"
let i = 1 > 2 ? 15 : 20;
"#
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
sgt r1 1 2
select r2 r1 15 20
move r8 r2
"
}
);
Ok(())
}
#[test]
fn test_ternary_expression_assignment() -> Result<()> {
let compiled = compile! {
debug
r#"
let i = 0;
i = 1 > 2 ? 15 : 20;
"#
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 0
sgt r1 1 2
select r2 r1 15 20
move r8 r2
" "
} }
); );

View File

@@ -20,10 +20,10 @@ fn test_if_statement() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 10 #a move r8 10
sgt r1 r8 5 sgt r1 r8 5
beq r1 0 L1 beqz r1 L1
move r8 20 #a move r8 20
L1: L1:
" "
} }
@@ -52,13 +52,13 @@ fn test_if_else_statement() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 0 #a move r8 0
sgt r1 10 5 sgt r1 10 5
beq r1 0 L2 beqz r1 L2
move r8 1 #a move r8 1
j L1 j L1
L2: L2:
move r8 2 #a move r8 2
L1: L1:
" "
} }
@@ -89,18 +89,18 @@ fn test_if_else_if_statement() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 0 #a move r8 0
seq r1 r8 1 seq r1 r8 1
beq r1 0 L2 beqz r1 L2
move r8 10 #a move r8 10
j L1 j L1
L2: L2:
seq r2 r8 2 seq r2 r8 2
beq r2 0 L4 beqz r2 L4
move r8 20 #a move r8 20
j L3 j L3
L4: L4:
move r8 30 #a move r8 30
L3: L3:
L1: L1:
" "
@@ -136,18 +136,18 @@ fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 1 #a move r8 1
move r9 2 #b move r9 2
move r10 3 #c move r10 3
move r11 4 #d move r11 4
move r12 5 #e move r12 5
move r13 6 #f move r13 6
move r14 7 #g move r14 7
push 8 #h push 8
seq r1 r8 1 seq r1 r8 1
beq r1 0 L1 beqz r1 L1
sub r0 sp 1 sub r0 sp 1
put db r0 99 #h put db r0 99
L1: L1:
sub sp sp 1 sub sp sp 1
" "

View File

@@ -17,13 +17,14 @@ fn no_arguments() -> anyhow::Result<()> {
j main j main
doSomething: doSomething:
push ra push ra
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 1 sub sp sp 1
j ra j ra
main: main:
jal doSomething jal doSomething
move r8 r15 #i move r8 r15
" "
}; };
@@ -34,14 +35,17 @@ 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
" "
fn doSomething(arg1) {}; fn mul2(arg1) {
let arg1 = 123; return arg1 * 2;
let i = doSomething(arg1); };
loop {
let arg1 = 123;
let i = mul2(arg1);
i = i ** 2;
}
" "
}; };
@@ -50,23 +54,31 @@ fn let_var_args() -> anyhow::Result<()> {
indoc! { indoc! {
" "
j main j main
doSomething: mul2:
pop r8 #arg1 pop r8
push ra push ra
mul r1 r8 2
move r15 r1
j L1
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 1 sub sp sp 1
j ra j ra
main: main:
move r8 123 #arg1 L2:
move r8 123
push r8 push r8
push r8 push r8
jal doSomething jal mul2
sub r0 sp 1 sub r0 sp 1
get r8 db r0 get r8 db r0
sub sp sp 1 sub sp sp 1
move r9 r15 #i move r9 r15
sub sp sp 1 pow r1 r9 2
move r9 r1
j L2
L3:
" "
} }
); );
@@ -97,7 +109,9 @@ fn inline_literal_args() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug debug
" "
fn doSomething(arg1, arg2) {}; fn doSomething(arg1, arg2) {
return 5;
};
let thisVariableShouldStayInPlace = 123; let thisVariableShouldStayInPlace = 123;
let returnedValue = doSomething(12, 34); let returnedValue = doSomething(12, 34);
" "
@@ -109,15 +123,18 @@ fn inline_literal_args() -> anyhow::Result<()> {
" "
j main j main
doSomething: doSomething:
pop r8 #arg2 pop r8
pop r9 #arg1 pop r9
push ra push ra
move r15 5
j L1
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 1 sub sp sp 1
j ra j ra
main: main:
move r8 123 #thisVariableShouldStayInPlace move r8 123
push r8 push r8
push 12 push 12
push 34 push 34
@@ -125,8 +142,7 @@ fn inline_literal_args() -> anyhow::Result<()> {
sub r0 sp 1 sub r0 sp 1
get r8 db r0 get r8 db r0
sub sp sp 1 sub sp sp 1
move r9 r15 #returnedValue move r9 r15
sub sp sp 1
" "
} }
); );
@@ -151,15 +167,16 @@ fn mixed_args() -> anyhow::Result<()> {
" "
j main j main
doSomething: doSomething:
pop r8 #arg2 pop r8
pop r9 #arg1 pop r9
push ra push ra
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 1 sub sp sp 1
j ra j ra
main: main:
move r8 123 #arg1 move r8 123
push r8 push r8
push r8 push r8
push 456 push 456
@@ -167,8 +184,7 @@ fn mixed_args() -> anyhow::Result<()> {
sub r0 sp 1 sub r0 sp 1
get r8 db r0 get r8 db r0
sub sp sp 1 sub sp sp 1
move r9 r15 #returnValue move r9 r15
sub sp sp 1
" "
} }
); );
@@ -195,9 +211,11 @@ fn with_return_statement() -> anyhow::Result<()> {
" "
j main j main
doSomething: doSomething:
pop r8 #arg1 pop r8
push ra push ra
move r15 456 #returnValue move r15 456
j L1
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 1 sub sp sp 1
@@ -205,7 +223,7 @@ fn with_return_statement() -> anyhow::Result<()> {
main: main:
push 123 push 123
jal doSomething jal doSomething
move r8 r15 #returned move r8 r15
" "
} }
); );
@@ -232,14 +250,15 @@ fn with_negative_return_literal() -> anyhow::Result<()> {
j main j main
doSomething: doSomething:
push ra push ra
move r15 -1 #returnValue move r15 -1
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 1 sub sp sp 1
j ra j ra
main: main:
jal doSomething jal doSomething
move r8 r15 #i move r8 r15
" "
} }
); );

View File

@@ -15,7 +15,7 @@ fn variable_declaration_numeric_literal() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 293.15 #i move r8 293.15
" "
} }
); );
@@ -46,16 +46,16 @@ fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()>
" "
j main j main
main: main:
move r8 0 #a move r8 0
move r9 1 #b move r9 1
move r10 2 #c move r10 2
move r11 3 #d move r11 3
move r12 4 #e move r12 4
move r13 5 #f move r13 5
move r14 6 #g move r14 6
push 7 #h push 7
push 8 #i push 8
push 9 #j push 9
sub sp sp 3 sub sp sp 3
" "
} }
@@ -79,7 +79,7 @@ fn variable_declaration_negative() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 -1 #i move r8 -1
" "
} }
); );
@@ -103,8 +103,8 @@ fn test_boolean_declaration() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 1 #t move r8 1
move r9 0 #f move r9 0
" "
} }
); );
@@ -132,14 +132,16 @@ fn test_boolean_return() -> anyhow::Result<()> {
j main j main
getTrue: getTrue:
push ra push ra
move r15 1 #returnValue move r15 1
j L1
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 1 sub sp sp 1
j ra j ra
main: main:
jal getTrue jal getTrue
move r8 r15 #val move r8 r15
" "
} }
); );

View File

@@ -13,14 +13,15 @@ fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
indoc! {" indoc! {"
j main j main
doSomething: doSomething:
pop r8 #arg9 pop r8
pop r9 #arg8 pop r9
pop r10 #arg7 pop r10
pop r11 #arg6 pop r11
pop r12 #arg5 pop r12
pop r13 #arg4 pop r13
pop r14 #arg3 pop r14
push ra push ra
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 3 sub sp sp 3
@@ -31,6 +32,48 @@ fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[test]
fn test_early_return() -> anyhow::Result<()> {
let compiled = compile!(debug r#"
// This is a test function declaration with no body
fn doSomething() {
if (1 == 1) {
return;
}
let i = 1 + 2;
return;
};
doSomething();
"#);
assert_eq!(
compiled,
indoc! {
"
j main
doSomething:
push ra
seq r1 1 1
beqz r1 L2
j L1
L2:
move r8 3
j L1
L1:
sub r0 sp 1
get ra db r0
sub sp sp 1
j ra
main:
jal doSomething
move r1 r15
"
}
);
Ok(())
}
#[test] #[test]
fn test_function_declaration_with_register_params() -> anyhow::Result<()> { fn test_function_declaration_with_register_params() -> anyhow::Result<()> {
let compiled = compile!(debug r#" let compiled = compile!(debug r#"
@@ -44,9 +87,10 @@ fn test_function_declaration_with_register_params() -> anyhow::Result<()> {
indoc! {" indoc! {"
j main j main
doSomething: doSomething:
pop r8 #arg2 pop r8
pop r9 #arg1 pop r9
push ra push ra
L1:
sub r0 sp 1 sub r0 sp 1
get ra db r0 get ra db r0
sub sp sp 1 sub sp sp 1

View File

@@ -23,17 +23,17 @@ fn test_comparison_expressions() -> anyhow::Result<()> {
j main j main
main: main:
sgt r1 10 5 sgt r1 10 5
move r8 r1 #isGreater move r8 r1
slt r2 5 10 slt r2 5 10
move r9 r2 #isLess move r9 r2
seq r3 5 5 seq r3 5 5
move r10 r3 #isEqual move r10 r3
sne r4 5 10 sne r4 5 10
move r11 r4 #isNotEqual move r11 r4
sge r5 10 10 sge r5 10 10
move r12 r5 #isGreaterOrEqual move r12 r5
sle r6 5 5 sle r6 5 5
move r13 r6 #isLessOrEqual move r13 r6
" "
} }
); );
@@ -59,11 +59,11 @@ fn test_logical_and_or_not() -> anyhow::Result<()> {
j main j main
main: main:
and r1 1 1 and r1 1 1
move r8 r1 #logic1 move r8 r1
or r2 1 0 or r2 1 0
move r9 r2 #logic2 move r9 r2
seq r3 1 0 seq r3 1 0
move r10 r3 #logic3 move r10 r3
" "
} }
); );
@@ -89,7 +89,7 @@ fn test_complex_logic() -> anyhow::Result<()> {
sgt r1 10 5 sgt r1 10 5
slt r2 5 10 slt r2 5 10
and r3 r1 r2 and r3 r1 r2
move r8 r3 #logic move r8 r3
" "
} }
); );
@@ -113,7 +113,7 @@ fn test_math_with_logic() -> anyhow::Result<()> {
j main j main
main: main:
sgt r1 3 1 sgt r1 3 1
move r8 r1 #logic move r8 r1
" "
} }
); );
@@ -137,7 +137,7 @@ fn test_boolean_in_logic() -> anyhow::Result<()> {
j main j main
main: main:
and r1 1 0 and r1 1 0
move r8 r1 #res move r8 r1
" "
} }
); );
@@ -163,11 +163,11 @@ fn test_invert_a_boolean() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 1 #i move r8 1
seq r1 r8 0 seq r1 r8 0
move r9 r1 #y move r9 r1
seq r2 r9 0 seq r2 r9 0
move r10 r2 #result move r10 r2
" "
} }
); );

View File

@@ -21,10 +21,10 @@ fn test_infinite_loop() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 0 #a move r8 0
L1: L1:
add r1 r8 1 add r1 r8 1
move r8 r1 #a move r8 r1
j L1 j L1
L2: L2:
" "
@@ -56,12 +56,12 @@ fn test_loop_break() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 0 #a move r8 0
L1: L1:
add r1 r8 1 add r1 r8 1
move r8 r1 #a move r8 r1
sgt r2 r8 10 sgt r2 r8 10
beq r2 0 L3 beqz r2 L3
j L2 j L2
L3: L3:
j L1 j L1
@@ -92,12 +92,12 @@ fn test_while_loop() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 0 #a move r8 0
L1: L1:
slt r1 r8 10 slt r1 r8 10
beq r1 0 L2 beqz r1 L2
add r2 r8 1 add r2 r8 1
move r8 r2 #a move r8 r2
j L1 j L1
L2: L2:
" "
@@ -130,12 +130,12 @@ fn test_loop_continue() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 0 #a move r8 0
L1: L1:
add r1 r8 1 add r1 r8 1
move r8 r1 #a move r8 r1
slt r2 r8 5 slt r2 r8 5
beq r2 0 L3 beqz r2 L3
j L1 j L1
L3: L3:
j L2 j L2

View File

@@ -19,7 +19,7 @@ fn test_acos() -> Result<()> {
j main j main
main: main:
acos r15 123 acos r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -43,7 +43,7 @@ fn test_asin() -> Result<()> {
j main j main
main: main:
asin r15 123 asin r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -67,7 +67,7 @@ fn test_atan() -> Result<()> {
j main j main
main: main:
atan r15 123 atan r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -91,7 +91,7 @@ fn test_atan2() -> Result<()> {
j main j main
main: main:
atan2 r15 123 456 atan2 r15 123 456
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -115,7 +115,7 @@ fn test_abs() -> Result<()> {
j main j main
main: main:
abs r15 -123 abs r15 -123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -139,7 +139,7 @@ fn test_ceil() -> Result<()> {
j main j main
main: main:
ceil r15 123.90 ceil r15 123.90
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -163,7 +163,7 @@ fn test_cos() -> Result<()> {
j main j main
main: main:
cos r15 123 cos r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -187,7 +187,7 @@ fn test_floor() -> Result<()> {
j main j main
main: main:
floor r15 123 floor r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -211,7 +211,7 @@ fn test_log() -> Result<()> {
j main j main
main: main:
log r15 123 log r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -235,7 +235,33 @@ fn test_max() -> Result<()> {
j main j main
main: main:
max r15 123 456 max r15 123 456
move r8 r15 #i move r8 r15
"
}
);
Ok(())
}
#[test]
fn test_max_from_game() -> Result<()> {
let compiled = compile! {
debug
r#"
let item = 0;
item = max(1 + 2, 2);
"#
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
move r8 0
max r15 3 2
move r8 r15
" "
} }
); );
@@ -259,7 +285,7 @@ fn test_min() -> Result<()> {
j main j main
main: main:
min r15 123 456 min r15 123 456
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -283,7 +309,7 @@ fn test_rand() -> Result<()> {
j main j main
main: main:
rand r15 rand r15
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -307,7 +333,7 @@ fn test_sin() -> Result<()> {
j main j main
main: main:
sin r15 3 sin r15 3
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -331,7 +357,7 @@ fn test_sqrt() -> Result<()> {
j main j main
main: main:
sqrt r15 3 sqrt r15 3
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -355,7 +381,7 @@ fn test_tan() -> Result<()> {
j main j main
main: main:
tan r15 3 tan r15 3
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -379,7 +405,7 @@ fn test_trunc() -> Result<()> {
j main j main
main: main:
trunc r15 3.234 trunc r15 3.234
move r8 r15 #i move r8 r15
" "
} }
); );

View File

@@ -12,31 +12,29 @@ macro_rules! compile {
let mut writer = std::io::BufWriter::new(Vec::new()); let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = ::Compiler::new( let compiler = ::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))), parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))),
&mut writer,
None, None,
); );
compiler.compile(); let res = compiler.compile();
res.instructions.write(&mut writer)?;
output!(writer) output!(writer)
}}; }};
(result $source:expr) => {{ (result $source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = crate::Compiler::new( let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))), parser::Parser::new(tokenizer::Tokenizer::from($source)),
&mut writer,
Some(crate::CompilerConfig { debug: true }), Some(crate::CompilerConfig { debug: true }),
); );
compiler.compile() compiler.compile().errors
}}; }};
(debug $source:expr) => {{ (debug $source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new()); let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = crate::Compiler::new( let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))), parser::Parser::new(tokenizer::Tokenizer::from($source)),
&mut writer,
Some(crate::CompilerConfig { debug: true }), Some(crate::CompilerConfig { debug: true }),
); );
compiler.compile(); let res = compiler.compile();
res.instructions.write(&mut writer)?;
output!(writer) output!(writer)
}}; }};
} }

View File

@@ -44,7 +44,7 @@ fn test_sleep() -> anyhow::Result<()> {
j main j main
main: main:
sleep 3 sleep 3
move r8 15 #sleepAmount move r8 15
sleep r8 sleep r8
mul r1 r8 2 mul r1 r8 2
sleep r1 sleep r1
@@ -73,7 +73,7 @@ fn test_set_on_device() -> anyhow::Result<()> {
" "
j main j main
main: main:
move r8 293.15 #internalTemp move r8 293.15
sgt r1 r8 298.15 sgt r1 r8 298.15
s d0 On r1 s d0 On r1
" "
@@ -150,7 +150,58 @@ fn test_load_from_device() -> anyhow::Result<()> {
j main j main
main: main:
l r15 d0 On l r15 d0 On
move r8 r15 #setting move r8 r15
"
}
);
Ok(())
}
#[test]
fn test_load_from_slot() -> anyhow::Result<()> {
let compiled = compile! {
debug
r#"
device airCon = "d0";
let setting = ls(airCon, 0, "Occupied");
"#
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
ls r15 d0 0 Occupied
move r8 r15
"
}
);
Ok(())
}
#[test]
fn test_set_slot() -> anyhow::Result<()> {
let compiled = compile! {
debug
r#"
device airCon = "d0";
ss(airCon, 0, "Occupied", true);
"#
};
assert_eq!(
compiled,
indoc! {
"
j main
main:
ss d0 0 Occupied 1
" "
} }
); );

File diff suppressed because it is too large Load Diff

View File

@@ -3,30 +3,31 @@
// r1 - r7 : Temporary Variables // r1 - r7 : Temporary Variables
// r8 - r14 : Persistant Variables // r8 - r14 : Persistant Variables
use helpers::Span;
use lsp_types::{Diagnostic, DiagnosticSeverity}; use lsp_types::{Diagnostic, DiagnosticSeverity};
use parser::tree_node::{Literal, Span}; use parser::tree_node::Literal;
use quick_error::quick_error; use std::{
use std::collections::{HashMap, VecDeque}; borrow::Cow,
collections::{HashMap, VecDeque},
};
use thiserror::Error;
const TEMP: [u8; 7] = [1, 2, 3, 4, 5, 6, 7]; const TEMP: [u8; 7] = [1, 2, 3, 4, 5, 6, 7];
const PERSIST: [u8; 7] = [8, 9, 10, 11, 12, 13, 14]; const PERSIST: [u8; 7] = [8, 9, 10, 11, 12, 13, 14];
quick_error! { #[derive(Error, Debug)]
#[derive(Debug)] pub enum Error<'a> {
pub enum Error { #[error("{0} already exists.")]
DuplicateVariable(var: String, span: Option<Span>) { DuplicateVariable(Cow<'a, str>, Option<Span>),
display("{var} already exists.")
} #[error("{0} does not exist.")]
UnknownVariable(var: String, span: Option<Span>) { UnknownVariable(Cow<'a, str>, Option<Span>),
display("{var} does not exist.")
} #[error("{0}")]
Unknown(reason: String, span: Option<Span>) { Unknown(Cow<'a, str>, Option<Span>),
display("{reason}")
}
}
} }
impl From<Error> for lsp_types::Diagnostic { impl<'a> From<Error<'a>> for lsp_types::Diagnostic {
fn from(value: Error) -> Self { fn from(value: Error) -> Self {
match value { match value {
Error::DuplicateVariable(_, span) Error::DuplicateVariable(_, span)
@@ -52,8 +53,8 @@ pub enum LocationRequest {
Stack, Stack,
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub enum VariableLocation { pub enum VariableLocation<'a> {
/// Represents a temporary register (r1 - r7) /// Represents a temporary register (r1 - r7)
Temporary(u8), Temporary(u8),
/// Represents a persistant register (r8 - r14) /// Represents a persistant register (r8 - r14)
@@ -61,20 +62,20 @@ pub enum VariableLocation {
/// 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. /// Represents a constant value and should be directly substituted as such.
Constant(Literal), Constant(Literal<'a>),
/// Represents a device pin. This will contain the exact `d0-d5` string /// Represents a device pin. This will contain the exact `d0-d5` string
Device(String), Device(Cow<'a, str>),
} }
pub struct VariableScope<'a> { pub struct VariableScope<'a, 'b> {
temporary_vars: VecDeque<u8>, temporary_vars: VecDeque<u8>,
persistant_vars: VecDeque<u8>, persistant_vars: VecDeque<u8>,
var_lookup_table: HashMap<String, VariableLocation>, var_lookup_table: HashMap<Cow<'a, str>, VariableLocation<'a>>,
stack_offset: u16, stack_offset: u16,
parent: Option<&'a VariableScope<'a>>, parent: Option<&'b VariableScope<'a, 'b>>,
} }
impl<'a> Default for VariableScope<'a> { impl<'a, 'b> Default for VariableScope<'a, 'b> {
fn default() -> Self { fn default() -> Self {
Self { Self {
parent: None, parent: None,
@@ -86,7 +87,7 @@ impl<'a> Default for VariableScope<'a> {
} }
} }
impl<'a> VariableScope<'a> { impl<'a, 'b> VariableScope<'a, 'b> {
#[allow(dead_code)] #[allow(dead_code)]
pub const TEMP_REGISTER_COUNT: u8 = 7; pub const TEMP_REGISTER_COUNT: u8 = 7;
pub const PERSIST_REGISTER_COUNT: u8 = 7; pub const PERSIST_REGISTER_COUNT: u8 = 7;
@@ -94,22 +95,24 @@ impl<'a> VariableScope<'a> {
pub const RETURN_REGISTER: u8 = 15; pub const RETURN_REGISTER: u8 = 15;
pub const TEMP_STACK_REGISTER: u8 = 0; pub const TEMP_STACK_REGISTER: u8 = 0;
pub fn registers(&self) -> impl Iterator<Item = &u8> { pub fn registers(&self) -> Vec<u8> {
self.var_lookup_table let mut used = Vec::new();
.values()
.filter(|val| { for r in TEMP {
matches!( if !self.temporary_vars.contains(&r) {
val, used.push(r);
VariableLocation::Temporary(_) | VariableLocation::Persistant(_) }
) }
})
.map(|loc| match loc { for r in PERSIST {
VariableLocation::Persistant(reg) | VariableLocation::Temporary(reg) => reg, if !self.persistant_vars.contains(&r) {
_ => unreachable!(), used.push(r);
}) }
}
used
} }
pub fn scoped(parent: &'a VariableScope<'a>) -> Self { pub fn scoped(parent: &'b VariableScope<'a, 'b>) -> Self {
Self { Self {
parent: Option::Some(parent), parent: Option::Some(parent),
temporary_vars: parent.temporary_vars.clone(), temporary_vars: parent.temporary_vars.clone(),
@@ -126,12 +129,11 @@ impl<'a> VariableScope<'a> {
/// to the stack. /// to the stack.
pub fn add_variable( pub fn add_variable(
&mut self, &mut self,
var_name: impl Into<String>, var_name: Cow<'a, str>,
location: LocationRequest, location: LocationRequest,
span: Option<Span>, span: Option<Span>,
) -> Result<VariableLocation, Error> { ) -> Result<VariableLocation<'a>, Error<'a>> {
let var_name = var_name.into(); if self.var_lookup_table.contains_key(&var_name) {
if self.var_lookup_table.contains_key(var_name.as_str()) {
return Err(Error::DuplicateVariable(var_name, span)); return Err(Error::DuplicateVariable(var_name, span));
} }
let var_location = match location { let var_location = match location {
@@ -166,11 +168,10 @@ impl<'a> VariableScope<'a> {
pub fn define_const( pub fn define_const(
&mut self, &mut self,
var_name: impl Into<String>, var_name: Cow<'a, str>,
value: Literal, value: Literal<'a>,
span: Option<Span>, span: Option<Span>,
) -> Result<VariableLocation, Error> { ) -> Result<VariableLocation<'a>, Error<'a>> {
let var_name = var_name.into();
if self.var_lookup_table.contains_key(&var_name) { if self.var_lookup_table.contains_key(&var_name) {
return Err(Error::DuplicateVariable(var_name, span)); return Err(Error::DuplicateVariable(var_name, span));
} }
@@ -183,13 +184,11 @@ impl<'a> VariableScope<'a> {
pub fn get_location_of( pub fn get_location_of(
&self, &self,
var_name: impl Into<String>, var_name: &Cow<'a, str>,
span: Option<Span>, span: Option<Span>,
) -> Result<VariableLocation, Error> { ) -> Result<VariableLocation<'a>, Error<'a>> {
let var_name = var_name.into();
// 1. Check this scope // 1. Check this scope
if let Some(var) = self.var_lookup_table.get(var_name.as_str()) { if let Some(var) = self.var_lookup_table.get(var_name) {
if let VariableLocation::Stack(inserted_at_offset) = var { if let VariableLocation::Stack(inserted_at_offset) = var {
// Return offset relative to CURRENT sp // Return offset relative to CURRENT sp
return Ok(VariableLocation::Stack( return Ok(VariableLocation::Stack(
@@ -210,7 +209,7 @@ impl<'a> VariableScope<'a> {
return Ok(loc); return Ok(loc);
} }
Err(Error::UnknownVariable(var_name, span)) Err(Error::UnknownVariable(var_name.clone(), span))
} }
pub fn has_parent(&self) -> bool { pub fn has_parent(&self) -> bool {
@@ -220,11 +219,10 @@ impl<'a> VariableScope<'a> {
#[allow(dead_code)] #[allow(dead_code)]
pub fn free_temp( pub fn free_temp(
&mut self, &mut self,
var_name: impl Into<String>, var_name: Cow<'a, str>,
span: Option<Span>, span: Option<Span>,
) -> Result<(), Error> { ) -> Result<(), Error<'a>> {
let var_name = var_name.into(); let Some(location) = self.var_lookup_table.remove(&var_name) else {
let Some(location) = self.var_lookup_table.remove(var_name.as_str()) else {
return Err(Error::UnknownVariable(var_name, span)); return Err(Error::UnknownVariable(var_name, span));
}; };
@@ -234,7 +232,7 @@ impl<'a> VariableScope<'a> {
} }
VariableLocation::Persistant(_) => { VariableLocation::Persistant(_) => {
return Err(Error::UnknownVariable( return Err(Error::UnknownVariable(
String::from("Attempted to free a `let` variable."), Cow::from("Attempted to free a `let` variable."),
span, span,
)); ));
} }

View File

@@ -0,0 +1,41 @@
// Pressure numbers are in KPa
device self = "db";
device emergencyRelief = "d0";
device greenhouseSensor = "d1";
device recycleValve = "d2";
const MAX_INTERIOR_PRESSURE = 80;
const MAX_INTERIOR_TEMP = 28c;
const MIN_INTERIOR_PRESSURE = 75;
const MIN_INTERIOR_TEMP = 25c;
const daylightSensor = 1076425094;
const growLight = hash("StructureGrowLight");
const wallLight = hash("StructureLightLong");
const lightRound = hash("StructureLightRound");
let shouldPurge = false;
loop {
let interiorPress = greenhouseSensor.Pressure;
let interiorTemp = greenhouseSensor.Temperature;
shouldPurge = (
interiorPress > MAX_INTERIOR_PRESSURE ||
interiorTemp > MAX_INTERIOR_TEMP
) || shouldPurge;
emergencyRelief.On = shouldPurge;
recycleValve.On = !shouldPurge;
if (shouldPurge && (interiorPress < MIN_INTERIOR_PRESSURE && interiorTemp < MIN_INTERIOR_TEMP)) {
shouldPurge = false;
}
let solarAngle = lb(daylightSensor, "SolarAngle", "Average");
let isDaylight = solarAngle < 90;
sb(growLight, "On", isDaylight);
sb(wallLight, "On", !isDaylight);
sb(lightRound, "On", !isDaylight);
}

View File

@@ -5,3 +5,4 @@ edition = "2024"
[dependencies] [dependencies]
crc32fast = { workspace = true } crc32fast = { workspace = true }
lsp-types = { workspace = true }

View File

@@ -2,6 +2,44 @@ mod helper_funcs;
mod macros; mod macros;
mod syscall; mod syscall;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
}
impl From<Span> for lsp_types::Range {
fn from(value: Span) -> Self {
Self {
start: lsp_types::Position {
line: value.start_line as u32,
character: value.start_col as u32,
},
end: lsp_types::Position {
line: value.end_line as u32,
character: value.end_col as u32,
},
}
}
}
impl From<&Span> for lsp_types::Range {
fn from(value: &Span) -> Self {
Self {
start: lsp_types::Position {
line: value.start_line as u32,
character: value.start_col as u32,
},
end: lsp_types::Position {
line: value.end_line as u32,
character: value.end_col as u32,
},
}
}
}
/// This trait will allow the LSP to emit documentation for various tokens and expressions. /// 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. /// You can easily create documentation for large enums with the `documented!` macro.
pub trait Documentation { pub trait Documentation {

View File

@@ -3,15 +3,10 @@ macro_rules! documented {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Internal Helper: Filter doc comments // 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]) => { (@doc_filter #[doc = $doc:expr]) => {
Some($doc) Some($doc)
}; };
// Case 2: Other attributes (derives, etc.). Return None.
// We catch any other token sequence inside the brackets.
(@doc_filter #[$($attr:tt)*]) => { (@doc_filter #[$($attr:tt)*]) => {
None None
}; };
@@ -30,23 +25,59 @@ macro_rules! documented {
}; };
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Main Macro Entry Point // Entry Point 1: Enum with a single Lifetime (e.g. enum Foo<'a>)
// -------------------------------------------------------------------------
(
$(#[$enum_attr:meta])* $vis:vis enum $name:ident < $lt:lifetime > {
$($body:tt)*
}
) => {
documented!(@generate
meta: [$(#[$enum_attr])*],
vis: [$vis],
name: [$name],
generics: [<$lt>],
body: [$($body)*]
);
};
// -------------------------------------------------------------------------
// Entry Point 2: Regular Enum (No Generics)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
( (
$(#[$enum_attr:meta])* $vis:vis enum $name:ident { $(#[$enum_attr:meta])* $vis:vis enum $name:ident {
$($body:tt)*
}
) => {
documented!(@generate
meta: [$(#[$enum_attr])*],
vis: [$vis],
name: [$name],
generics: [],
body: [$($body)*]
);
};
// -------------------------------------------------------------------------
// Code Generator (Shared Logic)
// -------------------------------------------------------------------------
(@generate
meta: [$(#[$enum_attr:meta])*],
vis: [$vis:vis],
name: [$name:ident],
generics: [$($generics:tt)*],
body: [
$( $(
// 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_attr:tt)* ])*
$variant:ident $variant:ident
$( ($($tuple:tt)*) )? $( ($($tuple:tt)*) )?
$( {$($structure:tt)*} )? $( {$($structure:tt)*} )?
),* $(,)? ),* $(,)?
} ]
) => { ) => {
// 1. Generate the actual Enum definition // 1. Generate the Enum Definition
$(#[$enum_attr])* $(#[$enum_attr])*
$vis enum $name { $vis enum $name $($generics)* {
$( $(
$(#[ $($variant_attr)* ])* $(#[ $($variant_attr)* ])*
$variant $variant
@@ -55,20 +86,19 @@ macro_rules! documented {
)* )*
} }
// 2. Implement the Documentation Trait // 2. Implement Documentation Trait
impl Documentation for $name { // We apply the captured generics (e.g., <'a>) to both the impl and the type
impl $($generics)* Documentation for $name $($generics)* {
fn docs(&self) -> String { fn docs(&self) -> String {
match self { match self {
$( $(
documented!(@arm $name $variant $( ($($tuple)*) )? $( {$($structure)*} )? ) => { documented!(@arm $name $variant $( ($($tuple)*) )? $( {$($structure)*} )? ) => {
// Create a temporary array of Option<&str> for all attributes
let doc_lines: &[Option<&str>] = &[ let doc_lines: &[Option<&str>] = &[
$( $(
documented!(@doc_filter #[ $($variant_attr)* ]) documented!(@doc_filter #[ $($variant_attr)* ])
),* ),*
]; ];
// Filter out the Nones (non-doc attributes), join, and return
doc_lines.iter() doc_lines.iter()
.filter_map(|&d| d) .filter_map(|&d| d)
.collect::<Vec<_>>() .collect::<Vec<_>>()
@@ -80,7 +110,6 @@ macro_rules! documented {
} }
} }
// 3. Implement Static Documentation Provider
#[allow(dead_code)] #[allow(dead_code)]
fn get_all_documentation() -> Vec<(&'static str, String)> { fn get_all_documentation() -> Vec<(&'static str, String)> {
vec![ vec![
@@ -88,7 +117,6 @@ macro_rules! documented {
( (
stringify!($variant), stringify!($variant),
{ {
// Re-use the same extraction logic
let doc_lines: &[Option<&str>] = &[ let doc_lines: &[Option<&str>] = &[
$( $(
documented!(@doc_filter #[ $($variant_attr)* ]) documented!(@doc_filter #[ $($variant_attr)* ])

View File

@@ -9,9 +9,11 @@ macro_rules! with_syscalls {
"load", "load",
"loadBatched", "loadBatched",
"loadBatchedNamed", "loadBatchedNamed",
"loadSlot",
"set", "set",
"setBatched", "setBatched",
"setBatchedNamed", "setBatchedNamed",
"setSlot",
"acos", "acos",
"asin", "asin",
"atan", "atan",
@@ -32,9 +34,11 @@ macro_rules! with_syscalls {
"l", "l",
"lb", "lb",
"lbn", "lbn",
"ls",
"s", "s",
"sb", "sb",
"sbn" "sbn",
"ss"
); );
}; };
} }

View File

@@ -0,0 +1,8 @@
[package]
name = "il"
version = "0.1.0"
edition = "2024"
[dependencies]
helpers = { path = "../helpers" }
rust_decimal = { workspace = true }

View File

@@ -0,0 +1,350 @@
use helpers::Span;
use rust_decimal::Decimal;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::io::{BufWriter, Write};
use std::ops::{Deref, DerefMut};
#[derive(Default)]
pub struct Instructions<'a>(Vec<InstructionNode<'a>>);
impl<'a> Deref for Instructions<'a> {
type Target = Vec<InstructionNode<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for Instructions<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Instructions<'a> {
pub fn new(instructions: Vec<InstructionNode<'a>>) -> Self {
Self(instructions)
}
pub fn into_inner(self) -> Vec<InstructionNode<'a>> {
self.0
}
pub fn write<W: Write>(self, writer: &mut BufWriter<W>) -> Result<(), std::io::Error> {
for node in self.0 {
writer.write_all(node.to_string().as_bytes())?;
writer.write_all(b"\n")?;
}
writer.flush()?;
Ok(())
}
pub fn source_map(&self) -> HashMap<usize, Span> {
let mut map = HashMap::new();
for (line_num, node) in self.0.iter().enumerate() {
if let Some(span) = node.span {
map.insert(line_num, span);
}
}
map
}
}
impl<'a> std::fmt::Display for Instructions<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for node in &self.0 {
writeln!(f, "{node}")?;
}
Ok(())
}
}
pub struct InstructionNode<'a> {
pub instruction: Instruction<'a>,
pub span: Option<Span>,
}
impl<'a> std::fmt::Display for InstructionNode<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.instruction)
}
}
impl<'a> InstructionNode<'a> {
pub fn new(instr: Instruction<'a>, span: Option<Span>) -> Self {
Self {
span,
instruction: instr,
}
}
}
/// Represents the different types of operands available in IC10.
#[derive(Debug, Clone, PartialEq)]
pub enum Operand<'a> {
/// A hardware register (r0-r15)
Register(u8),
/// A device alias or direct connection (d0-d5, db)
Device(Cow<'a, str>),
/// A numeric literal (integer or float)
Number(Decimal),
/// A label used for jumping
Label(Cow<'a, str>),
/// A logic type string (e.g., "Temperature", "Open")
LogicType(Cow<'a, str>),
/// Special register: Stack Pointer
StackPointer,
/// Special register: Return Address
ReturnAddress,
}
impl<'a> fmt::Display for Operand<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Operand::Register(r) => write!(f, "r{}", r),
Operand::Device(d) => write!(f, "{}", d),
Operand::Number(n) => write!(f, "{}", n),
Operand::Label(l) => write!(f, "{}", l),
Operand::LogicType(t) => write!(f, "{}", t),
Operand::StackPointer => write!(f, "sp"),
Operand::ReturnAddress => write!(f, "ra"),
}
}
}
/// Represents a single IC10 MIPS instruction.
#[derive(Debug, Clone, PartialEq)]
pub enum Instruction<'a> {
/// `move dst val` - Copy value to register
Move(Operand<'a>, Operand<'a>),
/// `add dst a b` - Addition
Add(Operand<'a>, Operand<'a>, Operand<'a>),
/// `sub dst a b` - Subtraction
Sub(Operand<'a>, Operand<'a>, Operand<'a>),
/// `mul dst a b` - Multiplication
Mul(Operand<'a>, Operand<'a>, Operand<'a>),
/// `div dst a b` - Division
Div(Operand<'a>, Operand<'a>, Operand<'a>),
/// `mod dst a b` - Modulo
Mod(Operand<'a>, Operand<'a>, Operand<'a>),
/// `pow dst a b` - Power
Pow(Operand<'a>, Operand<'a>, Operand<'a>),
/// `acos dst a`
Acos(Operand<'a>, Operand<'a>),
/// `asin dst a`
Asin(Operand<'a>, Operand<'a>),
/// `atan dst a`
Atan(Operand<'a>, Operand<'a>),
/// `atan2 dst a b`
Atan2(Operand<'a>, Operand<'a>, Operand<'a>),
/// `abs dst a`
Abs(Operand<'a>, Operand<'a>),
/// `ceil dst a`
Ceil(Operand<'a>, Operand<'a>),
/// `cos dst a`
Cos(Operand<'a>, Operand<'a>),
/// `floor dst a`
Floor(Operand<'a>, Operand<'a>),
/// `log dst a`
Log(Operand<'a>, Operand<'a>),
/// `max dst a b`
Max(Operand<'a>, Operand<'a>, Operand<'a>),
/// `min dst a b`
Min(Operand<'a>, Operand<'a>, Operand<'a>),
/// `rand dst`
Rand(Operand<'a>),
/// `sin dst a`
Sin(Operand<'a>, Operand<'a>),
/// `sqrt dst a`
Sqrt(Operand<'a>, Operand<'a>),
/// `tan dst a`
Tan(Operand<'a>, Operand<'a>),
/// `trunc dst a`
Trunc(Operand<'a>, Operand<'a>),
/// `l register device type` - Load from device
Load(Operand<'a>, Operand<'a>, Operand<'a>),
/// `s device type value` - Set on device
Store(Operand<'a>, Operand<'a>, Operand<'a>),
/// `ls register device slot type` - Load Slot
LoadSlot(Operand<'a>, Operand<'a>, Operand<'a>, Operand<'a>),
/// `ss device slot type value` - Set Slot
StoreSlot(Operand<'a>, Operand<'a>, Operand<'a>, Operand<'a>),
/// `lb register deviceHash type batchMode` - Load Batch
LoadBatch(Operand<'a>, Operand<'a>, Operand<'a>, Operand<'a>),
/// `sb deviceHash type value` - Set Batch
StoreBatch(Operand<'a>, Operand<'a>, Operand<'a>),
/// `lbn register deviceHash nameHash type batchMode` - Load Batch Named
LoadBatchNamed(
Operand<'a>,
Operand<'a>,
Operand<'a>,
Operand<'a>,
Operand<'a>,
),
/// `sbn deviceHash nameHash type value` - Set Batch Named
StoreBatchNamed(Operand<'a>, Operand<'a>, Operand<'a>, Operand<'a>),
/// `j label` - Unconditional Jump
Jump(Operand<'a>),
/// `jal label` - Jump and Link (Function Call)
JumpAndLink(Operand<'a>),
/// `jr offset` - Jump Relative
JumpRelative(Operand<'a>),
/// `beq a b label` - Branch if Equal
BranchEq(Operand<'a>, Operand<'a>, Operand<'a>),
/// `bne a b label` - Branch if Not Equal
BranchNe(Operand<'a>, Operand<'a>, Operand<'a>),
/// `bgt a b label` - Branch if Greater Than
BranchGt(Operand<'a>, Operand<'a>, Operand<'a>),
/// `blt a b label` - Branch if Less Than
BranchLt(Operand<'a>, Operand<'a>, Operand<'a>),
/// `bge a b label` - Branch if Greater or Equal
BranchGe(Operand<'a>, Operand<'a>, Operand<'a>),
/// `ble a b label` - Branch if Less or Equal
BranchLe(Operand<'a>, Operand<'a>, Operand<'a>),
/// `beqz a label` - Branch if Equal Zero
BranchEqZero(Operand<'a>, Operand<'a>),
/// `bnez a label` - Branch if Not Equal Zero
BranchNeZero(Operand<'a>, Operand<'a>),
/// `seq dst a b` - Set if Equal
SetEq(Operand<'a>, Operand<'a>, Operand<'a>),
/// `sne dst a b` - Set if Not Equal
SetNe(Operand<'a>, Operand<'a>, Operand<'a>),
/// `sgt dst a b` - Set if Greater Than
SetGt(Operand<'a>, Operand<'a>, Operand<'a>),
/// `slt dst a b` - Set if Less Than
SetLt(Operand<'a>, Operand<'a>, Operand<'a>),
/// `sge dst a b` - Set if Greater or Equal
SetGe(Operand<'a>, Operand<'a>, Operand<'a>),
/// `sle dst a b` - Set if Less or Equal
SetLe(Operand<'a>, Operand<'a>, Operand<'a>),
/// `and dst a b` - Logical AND
And(Operand<'a>, Operand<'a>, Operand<'a>),
/// `or dst a b` - Logical OR
Or(Operand<'a>, Operand<'a>, Operand<'a>),
/// `xor dst a b` - Logical XOR
Xor(Operand<'a>, Operand<'a>, Operand<'a>),
/// `push val` - Push to Stack
Push(Operand<'a>),
/// `pop dst` - Pop from Stack
Pop(Operand<'a>),
/// `peek dst` - Peek from Stack (Usually sp - 1)
Peek(Operand<'a>),
/// `get dst dev num`
Get(Operand<'a>, Operand<'a>, Operand<'a>),
/// put dev addr val
Put(Operand<'a>, Operand<'a>, Operand<'a>),
/// `select dst cond a b` - Ternary Select
Select(Operand<'a>, Operand<'a>, Operand<'a>, Operand<'a>),
/// `yield` - Pause execution
Yield,
/// `sleep val` - Sleep for seconds
Sleep(Operand<'a>),
/// `alias name target` - Define Alias (Usually handled by compiler, but good for IR)
Alias(Cow<'a, str>, Operand<'a>),
/// `define name val` - Define Constant (Usually handled by compiler)
Define(Cow<'a, str>, f64),
/// A label definition `Label:`
LabelDef(Cow<'a, str>),
/// A comment `# text`
Comment(Cow<'a, str>),
}
impl<'a> fmt::Display for Instruction<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Instruction::Move(dst, val) => write!(f, "move {} {}", dst, val),
Instruction::Add(dst, a, b) => write!(f, "add {} {} {}", dst, a, b),
Instruction::Sub(dst, a, b) => write!(f, "sub {} {} {}", dst, a, b),
Instruction::Mul(dst, a, b) => write!(f, "mul {} {} {}", dst, a, b),
Instruction::Div(dst, a, b) => write!(f, "div {} {} {}", dst, a, b),
Instruction::Mod(dst, a, b) => write!(f, "mod {} {} {}", dst, a, b),
Instruction::Pow(dst, a, b) => write!(f, "pow {} {} {}", dst, a, b),
Instruction::Acos(dst, a) => write!(f, "acos {} {}", dst, a),
Instruction::Asin(dst, a) => write!(f, "asin {} {}", dst, a),
Instruction::Atan(dst, a) => write!(f, "atan {} {}", dst, a),
Instruction::Atan2(dst, a, b) => write!(f, "atan2 {} {} {}", dst, a, b),
Instruction::Abs(dst, a) => write!(f, "abs {} {}", dst, a),
Instruction::Ceil(dst, a) => write!(f, "ceil {} {}", dst, a),
Instruction::Cos(dst, a) => write!(f, "cos {} {}", dst, a),
Instruction::Floor(dst, a) => write!(f, "floor {} {}", dst, a),
Instruction::Log(dst, a) => write!(f, "log {} {}", dst, a),
Instruction::Max(dst, a, b) => write!(f, "max {} {} {}", dst, a, b),
Instruction::Min(dst, a, b) => write!(f, "min {} {} {}", dst, a, b),
Instruction::Rand(dst) => write!(f, "rand {}", dst),
Instruction::Sin(dst, a) => write!(f, "sin {} {}", dst, a),
Instruction::Sqrt(dst, a) => write!(f, "sqrt {} {}", dst, a),
Instruction::Tan(dst, a) => write!(f, "tan {} {}", dst, a),
Instruction::Trunc(dst, a) => write!(f, "trunc {} {}", dst, a),
Instruction::Load(reg, dev, typ) => write!(f, "l {} {} {}", reg, dev, typ),
Instruction::Store(dev, typ, val) => write!(f, "s {} {} {}", dev, typ, val),
Instruction::LoadSlot(reg, dev, slot, typ) => {
write!(f, "ls {} {} {} {}", reg, dev, slot, typ)
}
Instruction::StoreSlot(dev, slot, typ, val) => {
write!(f, "ss {} {} {} {}", dev, slot, typ, val)
}
Instruction::LoadBatch(reg, hash, typ, mode) => {
write!(f, "lb {} {} {} {}", reg, hash, typ, mode)
}
Instruction::StoreBatch(hash, typ, val) => write!(f, "sb {} {} {}", hash, typ, val),
Instruction::LoadBatchNamed(reg, d_hash, n_hash, typ, mode) => {
write!(f, "lbn {} {} {} {} {}", reg, d_hash, n_hash, typ, mode)
}
Instruction::StoreBatchNamed(d_hash, n_hash, typ, val) => {
write!(f, "sbn {} {} {} {}", d_hash, n_hash, typ, val)
}
Instruction::Jump(lbl) => write!(f, "j {}", lbl),
Instruction::JumpAndLink(lbl) => write!(f, "jal {}", lbl),
Instruction::JumpRelative(off) => write!(f, "jr {}", off),
Instruction::BranchEq(a, b, lbl) => write!(f, "beq {} {} {}", a, b, lbl),
Instruction::BranchNe(a, b, lbl) => write!(f, "bne {} {} {}", a, b, lbl),
Instruction::BranchGt(a, b, lbl) => write!(f, "bgt {} {} {}", a, b, lbl),
Instruction::BranchLt(a, b, lbl) => write!(f, "blt {} {} {}", a, b, lbl),
Instruction::BranchGe(a, b, lbl) => write!(f, "bge {} {} {}", a, b, lbl),
Instruction::BranchLe(a, b, lbl) => write!(f, "ble {} {} {}", a, b, lbl),
Instruction::BranchEqZero(a, lbl) => write!(f, "beqz {} {}", a, lbl),
Instruction::BranchNeZero(a, lbl) => write!(f, "bnez {} {}", a, lbl),
Instruction::SetEq(dst, a, b) => write!(f, "seq {} {} {}", dst, a, b),
Instruction::SetNe(dst, a, b) => write!(f, "sne {} {} {}", dst, a, b),
Instruction::SetGt(dst, a, b) => write!(f, "sgt {} {} {}", dst, a, b),
Instruction::SetLt(dst, a, b) => write!(f, "slt {} {} {}", dst, a, b),
Instruction::SetGe(dst, a, b) => write!(f, "sge {} {} {}", dst, a, b),
Instruction::SetLe(dst, a, b) => write!(f, "sle {} {} {}", dst, a, b),
Instruction::And(dst, a, b) => write!(f, "and {} {} {}", dst, a, b),
Instruction::Or(dst, a, b) => write!(f, "or {} {} {}", dst, a, b),
Instruction::Xor(dst, a, b) => write!(f, "xor {} {} {}", dst, a, b),
Instruction::Push(val) => write!(f, "push {}", val),
Instruction::Pop(dst) => write!(f, "pop {}", dst),
Instruction::Peek(dst) => write!(f, "peek {}", dst),
Instruction::Get(dst, dev, val) => write!(f, "get {} {} {}", dst, dev, val),
Instruction::Put(dev, addr, val) => write!(f, "put {} {} {}", dev, addr, val),
Instruction::Select(dst, cond, a, b) => {
write!(f, "select {} {} {} {}", dst, cond, a, b)
}
Instruction::Yield => write!(f, "yield"),
Instruction::Sleep(val) => write!(f, "sleep {}", 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),
Instruction::Comment(c) => write!(f, "# {}", c),
}
}
}

View File

@@ -0,0 +1,10 @@
[package]
name = "optimizer"
version = "0.1.0"
edition = "2024"
[dependencies]
il = { path = "../il" }
helpers = { path = "../helpers" }
rust_decimal = { workspace = true }
anyhow = { workspace = true }

View File

@@ -0,0 +1,44 @@
use il::{Instruction, InstructionNode};
use std::collections::HashSet;
/// Scans the instruction set to identify "leaf functions".
/// A leaf function is defined as a function (delimited by LabelDefs) that does not
/// contain any `jal` (JumpAndLink) instructions.
///
/// Returns a Set containing the names of all identified leaf functions.
pub fn find_leaf_functions(instructions: &[InstructionNode]) -> HashSet<String> {
let mut leaf_functions = HashSet::new();
let mut current_label: Option<String> = None;
let mut is_current_leaf = true;
for node in instructions {
match &node.instruction {
Instruction::LabelDef(label) => {
// If we were tracking a function, and it remained a leaf until now, save it.
if let Some(name) = current_label.take()
&& is_current_leaf
{
leaf_functions.insert(name);
}
// Start tracking the new function
current_label = Some(label.to_string());
is_current_leaf = true;
}
Instruction::JumpAndLink(_) => {
// If we see a JAL, this function is NOT a leaf.
is_current_leaf = false;
}
_ => {}
}
}
// Handle the final function in the file
if let Some(name) = current_label
&& is_current_leaf
{
leaf_functions.insert(name);
}
leaf_functions
}

View File

@@ -0,0 +1,868 @@
use il::{Instruction, InstructionNode, Instructions, Operand};
use rust_decimal::Decimal;
use std::collections::{HashMap, HashSet};
mod leaf_function;
use leaf_function::find_leaf_functions;
/// Entry point for the optimizer.
pub fn optimize<'a>(instructions: Instructions<'a>) -> Instructions<'a> {
let mut instructions = instructions.into_inner();
let mut changed = true;
let mut pass_count = 0;
const MAX_PASSES: usize = 10;
// Iterative passes for code simplification
while changed && pass_count < MAX_PASSES {
changed = false;
pass_count += 1;
// Pass 1: Constant Propagation
let (new_inst, c1) = constant_propagation(instructions);
instructions = new_inst;
changed |= c1;
// Pass 2: Register Forwarding (Intermediate Move Elimination)
let (new_inst, c2) = register_forwarding(instructions);
instructions = new_inst;
changed |= c2;
// Pass 3: Function Call Optimization (Remove unused push/pop around calls)
let (new_inst, c3) = optimize_function_calls(instructions);
instructions = new_inst;
changed |= c3;
// Pass 4: Leaf Function Optimization (Remove RA save/restore for leaf functions)
// This is separate from pass 3 as it deals with the function *definition*, not the call site.
let (new_inst, c4) = optimize_leaf_functions(instructions);
instructions = new_inst;
changed |= c4;
// Pass 5: Redundant Move Elimination
let (new_inst, c5) = remove_redundant_moves(instructions);
instructions = new_inst;
changed |= c5;
// Pass 6: Dead Code Elimination
let (new_inst, c6) = remove_unreachable_code(instructions);
instructions = new_inst;
changed |= c6;
}
// Final Pass: Resolve Labels to Line Numbers
Instructions::new(resolve_labels(instructions))
}
/// Helper: Check if a function body contains unsafe stack manipulation.
/// Returns true if the function modifies SP in a way that makes static RA offset analysis unsafe.
fn function_has_complex_stack_ops(
instructions: &[InstructionNode],
start_idx: usize,
end_idx: usize,
) -> bool {
for instruction in instructions.iter().take(end_idx).skip(start_idx) {
match instruction.instruction {
Instruction::Push(_) | Instruction::Pop(_) => return true,
// Check for explicit SP modification
Instruction::Add(Operand::StackPointer, _, _)
| Instruction::Sub(Operand::StackPointer, _, _)
| Instruction::Mul(Operand::StackPointer, _, _)
| Instruction::Div(Operand::StackPointer, _, _)
| Instruction::Move(Operand::StackPointer, _) => return true,
_ => {}
}
}
false
}
/// Pass: Leaf Function Optimization
/// If a function makes no calls (is a leaf), it doesn't need to save/restore `ra`.
fn optimize_leaf_functions<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let leaves = find_leaf_functions(&input);
if leaves.is_empty() {
return (input, false);
}
let mut changed = false;
let mut to_remove = HashSet::new();
// We map function names to the INDEX of the instruction that restores RA.
// We use this to validate the function body later.
let mut func_restore_indices = HashMap::new();
let mut func_ra_offsets = HashMap::new();
let mut current_function: Option<String> = None;
let mut function_start_indices = HashMap::new();
// First scan: Identify instructions to remove and capture RA offsets
for (i, node) in input.iter().enumerate() {
match &node.instruction {
Instruction::LabelDef(label) => {
current_function = Some(label.to_string());
function_start_indices.insert(label.to_string(), i);
}
Instruction::Push(Operand::ReturnAddress) => {
if let Some(func) = &current_function
&& leaves.contains(func)
{
to_remove.insert(i);
}
}
Instruction::Get(Operand::ReturnAddress, _, Operand::Register(_)) => {
// This is the restore instruction: `get ra db r0`
if let Some(func) = &current_function
&& leaves.contains(func)
{
to_remove.insert(i);
func_restore_indices.insert(func.clone(), i);
// Look back for the address calc: `sub r0 sp OFFSET`
if i > 0
&& let Instruction::Sub(_, Operand::StackPointer, Operand::Number(n)) =
&input[i - 1].instruction
{
func_ra_offsets.insert(func.clone(), *n);
to_remove.insert(i - 1);
}
}
}
_ => {}
}
}
// Safety Check: Verify that functions marked for optimization don't have complex stack ops.
// If they do, unmark them.
let mut safe_functions = HashSet::new();
for (func, start_idx) in &function_start_indices {
if let Some(restore_idx) = func_restore_indices.get(func) {
// Check instructions between start and restore using the helper function.
// We need to skip the `push ra` we just marked for removal, otherwise the helper
// will flag it as a complex op (Push).
// `start_idx` is the LabelDef. `start_idx + 1` is typically `push ra`.
let check_start = if to_remove.contains(&(start_idx + 1)) {
start_idx + 2
} else {
start_idx + 1
};
// `restore_idx` points to the `get ra` instruction. The helper scans up to `end_idx` exclusive,
// so we don't need to worry about the restore instruction itself.
if !function_has_complex_stack_ops(&input, check_start, *restore_idx) {
safe_functions.insert(func.clone());
changed = true;
}
}
}
if !changed {
return (input, false);
}
// Second scan: Rebuild with adjustments, but only for SAFE functions
let mut output = Vec::with_capacity(input.len());
let mut processing_function: Option<String> = None;
for (i, mut node) in input.into_iter().enumerate() {
if to_remove.contains(&i)
&& let Some(func) = &processing_function
&& safe_functions.contains(func)
{
continue; // SKIP (Remove)
}
if let Instruction::LabelDef(l) = &node.instruction {
processing_function = Some(l.to_string());
}
// Apply Stack Adjustments
if let Some(func) = &processing_function
&& safe_functions.contains(func)
&& let Some(ra_offset) = func_ra_offsets.get(func)
{
// 1. Stack Cleanup Adjustment
if let Instruction::Sub(
Operand::StackPointer,
Operand::StackPointer,
Operand::Number(n),
) = &mut node.instruction
{
// Decrease cleanup amount by 1 (for the removed RA)
let new_n = *n - Decimal::from(1);
if new_n.is_zero() {
continue;
}
*n = new_n;
}
// 2. Stack Variable Offset Adjustment
// Since we verified the function is "Simple" (no nested stack mods),
// we can safely assume offsets > ra_offset need shifting.
if let Instruction::Sub(_, Operand::StackPointer, Operand::Number(n)) =
&mut node.instruction
&& *n > *ra_offset
{
*n -= Decimal::from(1);
}
}
output.push(node);
}
(output, true)
}
/// Analyzes which registers are written to by each function label.
fn analyze_clobbers(instructions: &[InstructionNode]) -> HashMap<String, HashSet<u8>> {
let mut clobbers = HashMap::new();
let mut current_label = None;
for node in instructions {
if let Instruction::LabelDef(label) = &node.instruction {
current_label = Some(label.to_string());
clobbers.insert(label.to_string(), HashSet::new());
}
if let Some(label) = &current_label
&& let Some(reg) = get_destination_reg(&node.instruction)
&& let Some(set) = clobbers.get_mut(label)
{
set.insert(reg);
}
}
clobbers
}
/// Pass: Function Call Optimization
/// Removes Push/Restore pairs surrounding a JAL if the target function does not clobber that register.
fn optimize_function_calls<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let clobbers = analyze_clobbers(&input);
let mut changed = false;
let mut to_remove = HashSet::new();
let mut stack_adjustments = HashMap::new();
let mut i = 0;
while i < input.len() {
if let Instruction::JumpAndLink(Operand::Label(target)) = &input[i].instruction {
let target_key = target.to_string();
if let Some(func_clobbers) = clobbers.get(&target_key) {
// 1. Identify Pushes immediately preceding the JAL
let mut pushes = Vec::new(); // (index, register)
let mut scan_back = i.saturating_sub(1);
while scan_back > 0 {
if to_remove.contains(&scan_back) {
scan_back -= 1;
continue;
}
if let Instruction::Push(Operand::Register(r)) = &input[scan_back].instruction {
pushes.push((scan_back, *r));
scan_back -= 1;
} else {
break;
}
}
// 2. Identify Restores immediately following the JAL
let mut restores = Vec::new(); // (index_of_get, register, index_of_sub)
let mut scan_fwd = i + 1;
while scan_fwd < input.len() {
// Skip 'sub r0 sp X'
if let Instruction::Sub(Operand::Register(0), Operand::StackPointer, _) =
&input[scan_fwd].instruction
{
// Check next instruction for the Get
if scan_fwd + 1 < input.len()
&& let Instruction::Get(Operand::Register(r), _, Operand::Register(0)) =
&input[scan_fwd + 1].instruction
{
restores.push((scan_fwd + 1, *r, scan_fwd));
scan_fwd += 2;
continue;
}
}
break;
}
// 3. Stack Cleanup
let cleanup_idx = scan_fwd;
let has_cleanup = if cleanup_idx < input.len() {
matches!(
input[cleanup_idx].instruction,
Instruction::Sub(
Operand::StackPointer,
Operand::StackPointer,
Operand::Number(_)
)
)
} else {
false
};
// SAFEGUARD: Check Counts!
// If we pushed r8 twice but only restored it once, we have an argument.
// We must ensure the number of pushes for each register MATCHES the number of restores.
let mut push_counts = HashMap::new();
for (_, r) in &pushes {
*push_counts.entry(*r).or_insert(0) += 1;
}
let mut restore_counts = HashMap::new();
for (_, r, _) in &restores {
*restore_counts.entry(*r).or_insert(0) += 1;
}
let counts_match = push_counts
.iter()
.all(|(reg, count)| restore_counts.get(reg).unwrap_or(&0) == count);
// Also check reverse to ensure we didn't restore something we didn't push (unlikely but possible)
let counts_match_reverse = restore_counts
.iter()
.all(|(reg, count)| push_counts.get(reg).unwrap_or(&0) == count);
// Clobber Check
let all_pushes_safe = pushes.iter().all(|(_, r)| !func_clobbers.contains(r));
if all_pushes_safe && has_cleanup && counts_match && counts_match_reverse {
// We can remove ALL found pushes/restores safely
for (p_idx, _) in pushes {
to_remove.insert(p_idx);
}
for (g_idx, _, s_idx) in restores {
to_remove.insert(g_idx);
to_remove.insert(s_idx);
}
// Reduce stack cleanup amount
let num_removed = push_counts.values().sum::<i32>() as i64;
stack_adjustments.insert(cleanup_idx, num_removed);
changed = true;
}
}
}
i += 1;
}
if changed {
let mut clean = Vec::with_capacity(input.len());
for (idx, mut node) in input.into_iter().enumerate() {
if to_remove.contains(&idx) {
continue;
}
// Apply stack adjustment
if let Some(reduction) = stack_adjustments.get(&idx)
&& let Instruction::Sub(dst, a, Operand::Number(n)) = &node.instruction
{
let new_n = n - Decimal::from(*reduction);
if new_n.is_zero() {
continue; // Remove the sub entirely if 0
}
node.instruction = Instruction::Sub(dst.clone(), a.clone(), Operand::Number(new_n));
}
clean.push(node);
}
return (clean, changed);
}
(input, false)
}
/// Pass: Register Forwarding
/// Eliminates intermediate moves by writing directly to the final destination.
/// Example: `l r1 d0 T` + `move r9 r1` -> `l r9 d0 T`
fn register_forwarding<'a>(
mut input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut changed = false;
let mut i = 0;
// We use a while loop to manually control index so we can peek ahead
while i < input.len().saturating_sub(1) {
let next_idx = i + 1;
// Check if current instruction defines a register
// and the NEXT instruction is a move from that register.
let forward_candidate = if let Some(def_reg) = get_destination_reg(&input[i].instruction) {
if let Instruction::Move(Operand::Register(dest_reg), Operand::Register(src_reg)) =
&input[next_idx].instruction
{
if *src_reg == def_reg {
// Candidate found: Instruction `i` defines `src_reg`, Instruction `i+1` moves `src_reg` to `dest_reg`.
// We can optimize if `src_reg` (the temp) is NOT used after this move.
Some((def_reg, *dest_reg))
} else {
None
}
} else {
None
}
} else {
None
};
if let Some((temp_reg, final_reg)) = forward_candidate {
// Check liveness: Is temp_reg used after i+1?
// We scan from i+2 onwards.
let mut temp_is_dead = true;
for node in input.iter().skip(i + 2) {
if reg_is_read(&node.instruction, temp_reg) {
temp_is_dead = false;
break;
}
// If the temp is redefined, then the old value is dead, so we are safe.
if let Some(redef) = get_destination_reg(&node.instruction)
&& redef == temp_reg
{
break;
}
// If we hit a label/jump, we assume liveness might leak (conservative safety)
if matches!(
node.instruction,
Instruction::LabelDef(_) | Instruction::Jump(_) | Instruction::JumpAndLink(_)
) {
temp_is_dead = false;
break;
}
}
if temp_is_dead {
// Perform the swap
// 1. Rewrite input[i] to write to final_reg
if let Some(new_instr) = set_destination_reg(&input[i].instruction, final_reg) {
input[i].instruction = new_instr;
// 2. Remove input[i+1] (The Move)
input.remove(next_idx);
changed = true;
// Don't increment i, re-evaluate current index (which is now a new neighbor)
continue;
}
}
}
i += 1;
}
(input, changed)
}
/// Pass: Resolve Labels
/// Converts all Jump/Branch labels to absolute line numbers and removes LabelDefs.
fn resolve_labels<'a>(input: Vec<InstructionNode<'a>>) -> Vec<InstructionNode<'a>> {
let mut label_map: HashMap<String, usize> = HashMap::new();
let mut line_number = 0;
// 1. Build Label Map (filtering out LabelDefs from the count)
for node in &input {
if let Instruction::LabelDef(name) = &node.instruction {
label_map.insert(name.to_string(), line_number);
} else {
line_number += 1;
}
}
let mut output = Vec::with_capacity(input.len());
// 2. Rewrite Jumps and Filter Labels
for mut node in input {
// Helper to get line number as Decimal operand
let get_line = |lbl: &Operand| -> Option<Operand<'a>> {
if let Operand::Label(name) = lbl {
label_map
.get(name.as_ref())
.map(|&l| Operand::Number(Decimal::from(l)))
} else {
None
}
};
match &mut node.instruction {
Instruction::LabelDef(_) => continue, // Strip labels
// Jumps
Instruction::Jump(op) => {
if let Some(num) = get_line(op) {
*op = num;
}
}
Instruction::JumpAndLink(op) => {
if let Some(num) = get_line(op) {
*op = num;
}
}
Instruction::BranchEq(_, _, op)
| Instruction::BranchNe(_, _, op)
| Instruction::BranchGt(_, _, op)
| Instruction::BranchLt(_, _, op)
| Instruction::BranchGe(_, _, op)
| Instruction::BranchLe(_, _, op) => {
if let Some(num) = get_line(op) {
*op = num;
}
}
Instruction::BranchEqZero(_, op) | Instruction::BranchNeZero(_, op) => {
if let Some(num) = get_line(op) {
*op = num;
}
}
_ => {}
}
output.push(node);
}
output
}
// --- Helpers for Register Analysis ---
fn get_destination_reg(instr: &Instruction) -> Option<u8> {
match instr {
Instruction::Move(Operand::Register(r), _)
| Instruction::Add(Operand::Register(r), _, _)
| Instruction::Sub(Operand::Register(r), _, _)
| Instruction::Mul(Operand::Register(r), _, _)
| Instruction::Div(Operand::Register(r), _, _)
| Instruction::Mod(Operand::Register(r), _, _)
| Instruction::Pow(Operand::Register(r), _, _)
| Instruction::Load(Operand::Register(r), _, _)
| Instruction::LoadSlot(Operand::Register(r), _, _, _)
| Instruction::LoadBatch(Operand::Register(r), _, _, _)
| Instruction::LoadBatchNamed(Operand::Register(r), _, _, _, _)
| Instruction::SetEq(Operand::Register(r), _, _)
| Instruction::SetNe(Operand::Register(r), _, _)
| Instruction::SetGt(Operand::Register(r), _, _)
| Instruction::SetLt(Operand::Register(r), _, _)
| Instruction::SetGe(Operand::Register(r), _, _)
| Instruction::SetLe(Operand::Register(r), _, _)
| Instruction::And(Operand::Register(r), _, _)
| Instruction::Or(Operand::Register(r), _, _)
| Instruction::Xor(Operand::Register(r), _, _)
| Instruction::Peek(Operand::Register(r))
| Instruction::Get(Operand::Register(r), _, _)
| Instruction::Select(Operand::Register(r), _, _, _)
| Instruction::Rand(Operand::Register(r))
| Instruction::Acos(Operand::Register(r), _)
| Instruction::Asin(Operand::Register(r), _)
| Instruction::Atan(Operand::Register(r), _)
| Instruction::Atan2(Operand::Register(r), _, _)
| Instruction::Abs(Operand::Register(r), _)
| Instruction::Ceil(Operand::Register(r), _)
| Instruction::Cos(Operand::Register(r), _)
| Instruction::Floor(Operand::Register(r), _)
| Instruction::Log(Operand::Register(r), _)
| Instruction::Max(Operand::Register(r), _, _)
| Instruction::Min(Operand::Register(r), _, _)
| Instruction::Sin(Operand::Register(r), _)
| Instruction::Sqrt(Operand::Register(r), _)
| Instruction::Tan(Operand::Register(r), _)
| Instruction::Trunc(Operand::Register(r), _)
| Instruction::Pop(Operand::Register(r)) => Some(*r),
_ => None,
}
}
fn set_destination_reg<'a>(instr: &Instruction<'a>, new_reg: u8) -> Option<Instruction<'a>> {
// Helper to easily recreate instruction with new dest
let r = Operand::Register(new_reg);
match instr {
Instruction::Move(_, b) => Some(Instruction::Move(r, b.clone())),
Instruction::Add(_, a, b) => Some(Instruction::Add(r, a.clone(), b.clone())),
Instruction::Sub(_, a, b) => Some(Instruction::Sub(r, a.clone(), b.clone())),
Instruction::Mul(_, a, b) => Some(Instruction::Mul(r, a.clone(), b.clone())),
Instruction::Div(_, a, b) => Some(Instruction::Div(r, a.clone(), b.clone())),
Instruction::Mod(_, a, b) => Some(Instruction::Mod(r, a.clone(), b.clone())),
Instruction::Pow(_, a, b) => Some(Instruction::Pow(r, a.clone(), b.clone())),
Instruction::Load(_, a, b) => Some(Instruction::Load(r, a.clone(), b.clone())),
Instruction::LoadSlot(_, a, b, c) => {
Some(Instruction::LoadSlot(r, a.clone(), b.clone(), c.clone()))
}
Instruction::LoadBatch(_, a, b, c) => {
Some(Instruction::LoadBatch(r, a.clone(), b.clone(), c.clone()))
}
Instruction::LoadBatchNamed(_, a, b, c, d) => Some(Instruction::LoadBatchNamed(
r,
a.clone(),
b.clone(),
c.clone(),
d.clone(),
)),
Instruction::SetEq(_, a, b) => Some(Instruction::SetEq(r, a.clone(), b.clone())),
Instruction::SetNe(_, a, b) => Some(Instruction::SetNe(r, a.clone(), b.clone())),
Instruction::SetGt(_, a, b) => Some(Instruction::SetGt(r, a.clone(), b.clone())),
Instruction::SetLt(_, a, b) => Some(Instruction::SetLt(r, a.clone(), b.clone())),
Instruction::SetGe(_, a, b) => Some(Instruction::SetGe(r, a.clone(), b.clone())),
Instruction::SetLe(_, a, b) => Some(Instruction::SetLe(r, a.clone(), b.clone())),
Instruction::And(_, a, b) => Some(Instruction::And(r, a.clone(), b.clone())),
Instruction::Or(_, a, b) => Some(Instruction::Or(r, a.clone(), b.clone())),
Instruction::Xor(_, a, b) => Some(Instruction::Xor(r, a.clone(), b.clone())),
Instruction::Peek(_) => Some(Instruction::Peek(r)),
Instruction::Get(_, a, b) => Some(Instruction::Get(r, a.clone(), b.clone())),
Instruction::Select(_, a, b, c) => {
Some(Instruction::Select(r, a.clone(), b.clone(), c.clone()))
}
Instruction::Rand(_) => Some(Instruction::Rand(r)),
Instruction::Pop(_) => Some(Instruction::Pop(r)),
// Math funcs
Instruction::Acos(_, a) => Some(Instruction::Acos(r, a.clone())),
Instruction::Asin(_, a) => Some(Instruction::Asin(r, a.clone())),
Instruction::Atan(_, a) => Some(Instruction::Atan(r, a.clone())),
Instruction::Atan2(_, a, b) => Some(Instruction::Atan2(r, a.clone(), b.clone())),
Instruction::Abs(_, a) => Some(Instruction::Abs(r, a.clone())),
Instruction::Ceil(_, a) => Some(Instruction::Ceil(r, a.clone())),
Instruction::Cos(_, a) => Some(Instruction::Cos(r, a.clone())),
Instruction::Floor(_, a) => Some(Instruction::Floor(r, a.clone())),
Instruction::Log(_, a) => Some(Instruction::Log(r, a.clone())),
Instruction::Max(_, a, b) => Some(Instruction::Max(r, a.clone(), b.clone())),
Instruction::Min(_, a, b) => Some(Instruction::Min(r, a.clone(), b.clone())),
Instruction::Sin(_, a) => Some(Instruction::Sin(r, a.clone())),
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())),
_ => None,
}
}
fn reg_is_read(instr: &Instruction, reg: u8) -> bool {
let check = |op: &Operand| matches!(op, Operand::Register(r) if *r == reg);
match instr {
Instruction::Move(_, a) => check(a),
Instruction::Add(_, a, b)
| Instruction::Sub(_, a, b)
| Instruction::Mul(_, a, b)
| Instruction::Div(_, a, b)
| Instruction::Mod(_, a, b)
| Instruction::Pow(_, a, b) => check(a) || check(b),
Instruction::Load(_, a, _) => check(a), // Load reads device? Device can be reg? Yes.
Instruction::Store(a, _, b) => check(a) || check(b),
Instruction::BranchEq(a, b, _)
| Instruction::BranchNe(a, b, _)
| Instruction::BranchGt(a, b, _)
| Instruction::BranchLt(a, b, _)
| Instruction::BranchGe(a, b, _)
| Instruction::BranchLe(a, b, _) => check(a) || check(b),
Instruction::BranchEqZero(a, _) | Instruction::BranchNeZero(a, _) => check(a),
Instruction::SetEq(_, a, b)
| Instruction::SetNe(_, a, b)
| Instruction::SetGt(_, a, b)
| Instruction::SetLt(_, a, b)
| Instruction::SetGe(_, a, b)
| Instruction::SetLe(_, a, b)
| Instruction::And(_, a, b)
| Instruction::Or(_, a, b)
| Instruction::Xor(_, a, b) => check(a) || check(b),
Instruction::Push(a) => check(a),
Instruction::Get(_, a, b) => check(a) || check(b),
Instruction::Put(a, b, c) => check(a) || check(b) || check(c),
Instruction::Select(_, a, b, c) => check(a) || check(b) || check(c),
Instruction::Sleep(a) => check(a),
// Math single arg
Instruction::Acos(_, a)
| Instruction::Asin(_, a)
| Instruction::Atan(_, a)
| Instruction::Abs(_, a)
| Instruction::Ceil(_, a)
| Instruction::Cos(_, a)
| Instruction::Floor(_, a)
| Instruction::Log(_, a)
| Instruction::Sin(_, a)
| Instruction::Sqrt(_, a)
| Instruction::Tan(_, a)
| Instruction::Trunc(_, a) => check(a),
// Math double arg
Instruction::Atan2(_, a, b) | Instruction::Max(_, a, b) | Instruction::Min(_, a, b) => {
check(a) || check(b)
}
_ => false,
}
}
/// --- Constant Propagation & Dead Code ---
fn constant_propagation<'a>(input: Vec<InstructionNode<'a>>) -> (Vec<InstructionNode<'a>>, bool) {
let mut output = Vec::with_capacity(input.len());
let mut changed = false;
let mut registers: [Option<Decimal>; 16] = [None; 16];
for mut node in input {
match &node.instruction {
Instruction::LabelDef(_) | Instruction::JumpAndLink(_) => registers = [None; 16],
_ => {}
}
let simplified = match &node.instruction {
Instruction::Move(dst, src) => resolve_value(src, &registers)
.map(|val| Instruction::Move(dst.clone(), Operand::Number(val))),
Instruction::Add(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| x + y),
Instruction::Sub(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| x - y),
Instruction::Mul(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| x * y),
Instruction::Div(dst, a, b) => {
try_fold_math(
dst,
a,
b,
&registers,
|x, y| if y.is_zero() { x } else { x / y },
)
}
Instruction::Mod(dst, a, b) => {
try_fold_math(
dst,
a,
b,
&registers,
|x, y| if y.is_zero() { x } else { x % y },
)
}
Instruction::BranchEq(a, b, l) => {
try_resolve_branch(a, b, l, &registers, |x, y| x == y)
}
Instruction::BranchNe(a, b, l) => {
try_resolve_branch(a, b, l, &registers, |x, y| x != y)
}
Instruction::BranchGt(a, b, l) => try_resolve_branch(a, b, l, &registers, |x, y| x > y),
Instruction::BranchLt(a, b, l) => try_resolve_branch(a, b, l, &registers, |x, y| x < y),
Instruction::BranchGe(a, b, l) => {
try_resolve_branch(a, b, l, &registers, |x, y| x >= y)
}
Instruction::BranchLe(a, b, l) => {
try_resolve_branch(a, b, l, &registers, |x, y| x <= y)
}
Instruction::BranchEqZero(a, l) => {
try_resolve_branch(a, &Operand::Number(0.into()), l, &registers, |x, y| x == y)
}
Instruction::BranchNeZero(a, l) => {
try_resolve_branch(a, &Operand::Number(0.into()), l, &registers, |x, y| x != y)
}
_ => None,
};
if let Some(new) = simplified {
node.instruction = new;
changed = true;
}
// Update tracking
match &node.instruction {
Instruction::Move(Operand::Register(r), src) => {
registers[*r as usize] = resolve_value(src, &registers)
}
// Invalidate if destination is register
_ => {
if let Some(r) = get_destination_reg(&node.instruction) {
registers[r as usize] = None;
}
}
}
// Filter out NOPs (Empty LabelDefs from branch resolution)
if let Instruction::LabelDef(l) = &node.instruction
&& l.is_empty()
{
changed = true;
continue;
}
output.push(node);
}
(output, changed)
}
fn resolve_value(op: &Operand, regs: &[Option<Decimal>; 16]) -> Option<Decimal> {
match op {
Operand::Number(n) => Some(*n),
Operand::Register(r) => regs[*r as usize],
_ => None,
}
}
fn try_fold_math<'a, F>(
dst: &Operand<'a>,
a: &Operand<'a>,
b: &Operand<'a>,
regs: &[Option<Decimal>; 16],
op: F,
) -> Option<Instruction<'a>>
where
F: Fn(Decimal, Decimal) -> Decimal,
{
let val_a = resolve_value(a, regs)?;
let val_b = resolve_value(b, regs)?;
Some(Instruction::Move(
dst.clone(),
Operand::Number(op(val_a, val_b)),
))
}
fn try_resolve_branch<'a, F>(
a: &Operand<'a>,
b: &Operand<'a>,
label: &Operand<'a>,
regs: &[Option<Decimal>; 16],
check: F,
) -> Option<Instruction<'a>>
where
F: Fn(Decimal, Decimal) -> bool,
{
let val_a = resolve_value(a, regs)?;
let val_b = resolve_value(b, regs)?;
if check(val_a, val_b) {
Some(Instruction::Jump(label.clone()))
} else {
Some(Instruction::LabelDef("".into())) // NOP
}
}
fn remove_redundant_moves<'a>(input: Vec<InstructionNode<'a>>) -> (Vec<InstructionNode<'a>>, bool) {
let mut output = Vec::with_capacity(input.len());
let mut changed = false;
for node in input {
if let Instruction::Move(dst, src) = &node.instruction
&& dst == src
{
changed = true;
continue;
}
output.push(node);
}
(output, changed)
}
fn remove_unreachable_code<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut output = Vec::with_capacity(input.len());
let mut changed = false;
let mut dead = false;
for node in input {
if let Instruction::LabelDef(_) = node.instruction {
dead = false;
}
if dead {
changed = true;
continue;
}
if let Instruction::Jump(_) = node.instruction {
dead = true
}
output.push(node);
}
(output, changed)
}

View File

@@ -4,10 +4,11 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
quick-error = { workspace = true }
tokenizer = { path = "../tokenizer" } tokenizer = { path = "../tokenizer" }
helpers = { path = "../helpers" } helpers = { path = "../helpers" }
lsp-types = { workspace = true } lsp-types = { workspace = true }
safer-ffi = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies] [dev-dependencies]

File diff suppressed because it is too large Load Diff

View File

@@ -4,73 +4,73 @@ use helpers::prelude::*;
documented! { documented! {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Math { pub enum Math<'a> {
/// Returns the angle in radians whose cosine is the specified number. /// Returns the angle in radians whose cosine is the specified number.
/// ## IC10 /// ## IC10
/// `acos r? a(r?|num)` /// `acos r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = acos(number|var|expression);` /// `let item = acos(number|var|expression);`
Acos(Box<Spanned<Expression>>), Acos(Box<Spanned<Expression<'a>>>),
/// Returns the angle in radians whose sine is the specified number. /// Returns the angle in radians whose sine is the specified number.
/// ## IC10 /// ## IC10
/// `asin r? a(r?|num)` /// `asin r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = asin(number|var|expression);` /// `let item = asin(number|var|expression);`
Asin(Box<Spanned<Expression>>), Asin(Box<Spanned<Expression<'a>>>),
/// Returns the angle in radians whose tangent is the specified number. /// Returns the angle in radians whose tangent is the specified number.
/// ## IC10 /// ## IC10
/// `atan r? a(r?|num)` /// `atan r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = atan(number|var|expression);` /// `let item = atan(number|var|expression);`
Atan(Box<Spanned<Expression>>), Atan(Box<Spanned<Expression<'a>>>),
/// Returns the angle in radians whose tangent is the quotient of the specified numbers. /// Returns the angle in radians whose tangent is the quotient of the specified numbers.
/// ## IC10 /// ## IC10
/// `atan2 r? a(r?|num) b(r?|num)` /// `atan2 r? a(r?|num) b(r?|num)`
/// ## Slang /// ## Slang
/// `let item = atan2((number|var|expression), (number|var|expression));` /// `let item = atan2((number|var|expression), (number|var|expression));`
Atan2(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Atan2(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
/// Gets the absolute value of a number. /// Gets the absolute value of a number.
/// ## IC10 /// ## IC10
/// `abs r? a(r?|num)` /// `abs r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = abs((number|var|expression));` /// `let item = abs((number|var|expression));`
Abs(Box<Spanned<Expression>>), Abs(Box<Spanned<Expression<'a>>>),
/// Rounds a number up to the nearest whole number. /// Rounds a number up to the nearest whole number.
/// ## IC10 /// ## IC10
/// `ceil r? a(r?|num)` /// `ceil r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = ceil((number|var|expression));` /// `let item = ceil((number|var|expression));`
Ceil(Box<Spanned<Expression>>), Ceil(Box<Spanned<Expression<'a>>>),
/// Returns the cosine of the specified angle in radians. /// Returns the cosine of the specified angle in radians.
/// ## IC10 /// ## IC10
/// `cos r? a(r?|num)` /// `cos r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = cos((number|var|expression));` /// `let item = cos((number|var|expression));`
Cos(Box<Spanned<Expression>>), Cos(Box<Spanned<Expression<'a>>>),
/// Rounds a number down to the nearest whole number. /// Rounds a number down to the nearest whole number.
/// ## IC10 /// ## IC10
/// `floor r? a(r?|num)` /// `floor r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = floor((number|var|expression));` /// `let item = floor((number|var|expression));`
Floor(Box<Spanned<Expression>>), Floor(Box<Spanned<Expression<'a>>>),
/// Computes the natural logarithm of a number. /// Computes the natural logarithm of a number.
/// ## IC10 /// ## IC10
/// `log r? a(r?|num)` /// `log r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = log((number|var|expression));` /// `let item = log((number|var|expression));`
Log(Box<Spanned<Expression>>), Log(Box<Spanned<Expression<'a>>>),
/// Computes the maximum of two numbers. /// Computes the maximum of two numbers.
/// ## IC10 /// ## IC10
/// `max r? a(r?|num) b(r?|num)` /// `max r? a(r?|num) b(r?|num)`
/// ## Slang /// ## Slang
/// `let item = max((number|var|expression), (number|var|expression));` /// `let item = max((number|var|expression), (number|var|expression));`
Max(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Max(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
/// Computes the minimum of two numbers. /// Computes the minimum of two numbers.
/// ## IC10 /// ## IC10
/// `min r? a(r?|num) b(r?|num)` /// `min r? a(r?|num) b(r?|num)`
/// ## Slang /// ## Slang
/// `let item = min((number|var|expression), (number|var|expression));` /// `let item = min((number|var|expression), (number|var|expression));`
Min(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Min(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
/// Gets a random number between 0 and 1. /// Gets a random number between 0 and 1.
/// ## IC10 /// ## IC10
/// `rand r?` /// `rand r?`
@@ -82,29 +82,29 @@ documented! {
/// `sin r? a(r?|num)` /// `sin r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = sin((number|var|expression));` /// `let item = sin((number|var|expression));`
Sin(Box<Spanned<Expression>>), Sin(Box<Spanned<Expression<'a>>>),
/// Computes the square root of a number. /// Computes the square root of a number.
/// ## IC10 /// ## IC10
/// `sqrt r? a(r?|num)` /// `sqrt r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = sqrt((number|var|expression));` /// `let item = sqrt((number|var|expression));`
Sqrt(Box<Spanned<Expression>>), Sqrt(Box<Spanned<Expression<'a>>>),
/// Returns the tangent of the specified angle in radians. /// Returns the tangent of the specified angle in radians.
/// ## IC10 /// ## IC10
/// `tan r? a(r?|num)` /// `tan r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = tan((number|var|expression));` /// `let item = tan((number|var|expression));`
Tan(Box<Spanned<Expression>>), Tan(Box<Spanned<Expression<'a>>>),
/// Truncates a number by removing the decimal portion. /// Truncates a number by removing the decimal portion.
/// ## IC10 /// ## IC10
/// `trunc r? a(r?|num)` /// `trunc r? a(r?|num)`
/// ## Slang /// ## Slang
/// `let item = trunc((number|var|expression));` /// `let item = trunc((number|var|expression));`
Trunc(Box<Spanned<Expression>>), Trunc(Box<Spanned<Expression<'a>>>),
} }
} }
impl std::fmt::Display for Math { impl<'a> std::fmt::Display for Math<'a> {
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 {
Math::Acos(a) => write!(f, "acos({})", a), Math::Acos(a) => write!(f, "acos({})", a),
@@ -129,7 +129,7 @@ impl std::fmt::Display for Math {
documented! { documented! {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum System { pub enum System<'a> {
/// Pauses execution for exactly 1 tick and then resumes. /// Pauses execution for exactly 1 tick and then resumes.
/// ## IC10 /// ## IC10
/// `yield` /// `yield`
@@ -141,7 +141,7 @@ documented! {
/// `sleep a(r?|num)` /// `sleep a(r?|num)`
/// ## Slang /// ## Slang
/// `sleep(number|var);` /// `sleep(number|var);`
Sleep(Box<Spanned<Expression>>), Sleep(Box<Spanned<Expression<'a>>>),
/// Gets the in-game hash for a specific prefab name. NOTE! This call is COMPLETELY /// 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 /// 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. /// however, the hash is correctly computed at compile time and substitued automatically.
@@ -155,7 +155,7 @@ documented! {
/// const compDoor = hash("StructureCompositeDoor"); /// const compDoor = hash("StructureCompositeDoor");
/// setOnDeviceBatched(compDoor, "Lock", true); /// setOnDeviceBatched(compDoor, "Lock", true);
/// ``` /// ```
Hash(Spanned<Literal>), Hash(Spanned<Literal<'a>>),
/// Represents a function which loads a device variable into a register. /// Represents a function which loads a device variable into a register.
/// ## IC10 /// ## IC10
/// `l r? d? var` /// `l r? d? var`
@@ -163,7 +163,7 @@ documented! {
/// `let item = load(deviceHash, "LogicType");` /// `let item = load(deviceHash, "LogicType");`
/// `let item = l(deviceHash, "LogicType");` /// `let item = l(deviceHash, "LogicType");`
/// `let item = deviceAlias.LogicType;` /// `let item = deviceAlias.LogicType;`
LoadFromDevice(Spanned<LiteralOrVariable>, Spanned<Literal>), LoadFromDevice(Spanned<LiteralOrVariable<'a>>, Spanned<Literal<'a>>),
/// Function which gets a LogicType from all connected network devices that match /// Function which gets a LogicType from all connected network devices that match
/// the provided device hash and name, aggregating them via a batchMode /// the provided device hash and name, aggregating them via a batchMode
/// ## IC10 /// ## IC10
@@ -172,10 +172,10 @@ documented! {
/// `loadBatchedNamed(deviceHash, deviceName, "LogicType", "BatchMode");` /// `loadBatchedNamed(deviceHash, deviceName, "LogicType", "BatchMode");`
/// `lbn(deviceHash, deviceName, "LogicType", "BatchMode");` /// `lbn(deviceHash, deviceName, "LogicType", "BatchMode");`
LoadBatchNamed( LoadBatchNamed(
Spanned<LiteralOrVariable>, Spanned<LiteralOrVariable<'a>>,
Spanned<LiteralOrVariable>, Spanned<LiteralOrVariable<'a>>,
Spanned<Literal>, Spanned<Literal<'a>>,
Spanned<Literal>, Spanned<Literal<'a>>,
), ),
/// Loads a LogicType from all connected network devices, aggregating them via a /// Loads a LogicType from all connected network devices, aggregating them via a
/// BatchMode /// BatchMode
@@ -184,7 +184,7 @@ documented! {
/// ## Slang /// ## Slang
/// `loadBatched(deviceHash, "Variable", "LogicType");` /// `loadBatched(deviceHash, "Variable", "LogicType");`
/// `lb(deviceHash, "Variable", "LogicType");` /// `lb(deviceHash, "Variable", "LogicType");`
LoadBatch(Spanned<LiteralOrVariable>, Spanned<Literal>, Spanned<Literal>), LoadBatch(Spanned<LiteralOrVariable<'a>>, Spanned<Literal<'a>>, Spanned<Literal<'a>>),
/// Represents a function which stores a setting into a specific device. /// Represents a function which stores a setting into a specific device.
/// ## IC10 /// ## IC10
/// `s d? logicType r?` /// `s d? logicType r?`
@@ -192,7 +192,7 @@ documented! {
/// `set(deviceHash, "LogicType", (number|var));` /// `set(deviceHash, "LogicType", (number|var));`
/// `s(deviceHash, "LogicType", (number|var));` /// `s(deviceHash, "LogicType", (number|var));`
/// `deviceAlias.LogicType = (number|var);` /// `deviceAlias.LogicType = (number|var);`
SetOnDevice(Spanned<LiteralOrVariable>, Spanned<Literal>, Box<Spanned<Expression>>), SetOnDevice(Spanned<LiteralOrVariable<'a>>, Spanned<Literal<'a>>, Box<Spanned<Expression<'a>>>),
/// 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 /// the given deviceHash
/// ## IC10 /// ## IC10
@@ -200,7 +200,7 @@ documented! {
/// ## Slang /// ## Slang
/// `setBatched(deviceHash, "LogicType", (number|var));` /// `setBatched(deviceHash, "LogicType", (number|var));`
/// `sb(deviceHash, "LogicType", (number|var));` /// `sb(deviceHash, "LogicType", (number|var));`
SetOnDeviceBatched(Spanned<LiteralOrVariable>, Spanned<Literal>, Box<Spanned<Expression>>), SetOnDeviceBatched(Spanned<LiteralOrVariable<'a>>, Spanned<Literal<'a>>, Box<Spanned<Expression<'a>>>),
/// Represents a function which stores a setting to all devices that match /// Represents a function which stores a setting to all devices that match
/// both the given deviceHash AND the given nameHash /// both the given deviceHash AND the given nameHash
/// ## IC10 /// ## IC10
@@ -209,15 +209,39 @@ documented! {
/// `setBatchedNamed(deviceHash, nameHash, "LogicType", (number|var));` /// `setBatchedNamed(deviceHash, nameHash, "LogicType", (number|var));`
/// `sbn(deviceHash, nameHash, "LogicType", (number|var));` /// `sbn(deviceHash, nameHash, "LogicType", (number|var));`
SetOnDeviceBatchedNamed( SetOnDeviceBatchedNamed(
Spanned<LiteralOrVariable>, Spanned<LiteralOrVariable<'a>>,
Spanned<LiteralOrVariable>, Spanned<LiteralOrVariable<'a>>,
Spanned<Literal>, Spanned<Literal<'a>>,
Box<Spanned<Expression>>, Box<Spanned<Expression<'a>>>,
), ),
/// Loads slot LogicSlotType from device into a variable
///
/// ## IC10
/// `ls r0 d0 2 Occupied`
/// ## Slang
/// `let isOccupied = loadSlot(deviceHash, 2, "Occupied");`
/// `let isOccupied = ls(deviceHash, 2, "Occupied");`
LoadSlot(
Spanned<LiteralOrVariable<'a>>,
Spanned<Literal<'a>>,
Spanned<Literal<'a>>
),
/// Stores a value of LogicType on a device by the index value
/// ## IC10
/// `ss d0 0 "Open" 1`
/// ## Slang
/// `setSlot(deviceHash, 0, "Open", true);`
/// `ss(deviceHash, 0, "Open", true);`
SetSlot(
Spanned<LiteralOrVariable<'a>>,
Spanned<Literal<'a>>,
Spanned<Literal<'a>>,
Box<Spanned<Expression<'a>>>
)
} }
} }
impl std::fmt::Display for System { impl<'a> std::fmt::Display for System<'a> {
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 {
System::Yield => write!(f, "yield()"), System::Yield => write!(f, "yield()"),
@@ -235,6 +259,8 @@ impl std::fmt::Display for System {
System::SetOnDeviceBatchedNamed(a, b, c, d) => { System::SetOnDeviceBatchedNamed(a, b, c, d) => {
write!(f, "setOnDeviceBatchedNamed({}, {}, {}, {})", a, b, c, d) write!(f, "setOnDeviceBatchedNamed({}, {}, {}, {})", a, b, c, d)
} }
System::LoadSlot(a, b, c) => write!(f, "loadSlot({}, {}, {})", a, b, c),
System::SetSlot(a, b, c, d) => write!(f, "setSlot({}, {}, {}, {})", a, b, c, d),
} }
} }
} }
@@ -242,13 +268,13 @@ impl std::fmt::Display for System {
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
/// This represents built in functions that cannot be overwritten, but can be invoked by the user as functions. /// This represents built in functions that cannot be overwritten, but can be invoked by the user as functions.
pub enum SysCall { pub enum SysCall<'a> {
System(System), System(System<'a>),
/// Represents any mathmatical function that can be called. /// Represents any mathmatical function that can be called.
Math(Math), Math(Math<'a>),
} }
impl Documentation for SysCall { impl<'a> Documentation for SysCall<'a> {
fn docs(&self) -> String { fn docs(&self) -> String {
match self { match self {
Self::System(s) => s.docs(), Self::System(s) => s.docs(),
@@ -264,7 +290,7 @@ impl Documentation for SysCall {
} }
} }
impl std::fmt::Display for SysCall { impl<'a> std::fmt::Display for SysCall<'a> {
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 {
SysCall::System(s) => write!(f, "{}", s), SysCall::System(s) => write!(f, "{}", s),
@@ -273,7 +299,7 @@ impl std::fmt::Display for SysCall {
} }
} }
impl SysCall { impl<'a> SysCall<'a> {
pub fn is_syscall(identifier: &str) -> bool { pub fn is_syscall(identifier: &str) -> bool {
tokenizer::token::is_syscall(identifier) tokenizer::token::is_syscall(identifier)
} }

View File

@@ -160,3 +160,37 @@ fn test_negative_literal_const() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
fn test_ternary_expression() -> Result<()> {
let expr = parser!(r#"let i = x ? 1 : 2;"#).parse()?.unwrap();
assert_eq!("(let i = (x ? 1 : 2))", expr.to_string());
Ok(())
}
#[test]
fn test_complex_binary_with_ternary() -> Result<()> {
let expr = parser!("let i = (x ? 1 : 3) * 2;").parse()?.unwrap();
assert_eq!("(let i = ((x ? 1 : 3) * 2))", expr.to_string());
Ok(())
}
#[test]
fn test_operator_prescedence_with_ternary() -> Result<()> {
let expr = parser!("let x = x ? 1 : 3 * 2;").parse()?.unwrap();
assert_eq!("(let x = (x ? 1 : (3 * 2)))", expr.to_string());
Ok(())
}
#[test]
fn test_nested_ternary_right_associativity() -> Result<()> {
let expr = parser!("let i = a ? b : c ? d : e;").parse()?.unwrap();
assert_eq!("(let i = (a ? b : (c ? d : e)))", expr.to_string());
Ok(())
}

View File

@@ -1,24 +1,24 @@
use std::ops::Deref;
use crate::sys_call;
use super::sys_call::SysCall; use super::sys_call::SysCall;
use crate::sys_call;
use helpers::Span;
use safer_ffi::prelude::*;
use std::{borrow::Cow, ops::Deref};
use tokenizer::token::Number; use tokenizer::token::Number;
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub enum Literal { pub enum Literal<'a> {
Number(Number), Number(Number),
String(String), String(Cow<'a, str>),
Boolean(bool), Boolean(bool),
} }
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub enum LiteralOr<T> { pub enum LiteralOr<'a, T> {
Literal(Spanned<Literal>), Literal(Spanned<Literal<'a>>),
Or(Spanned<T>), Or(Spanned<T>),
} }
impl<T: std::fmt::Display> std::fmt::Display for LiteralOr<T> { impl<'a, T: std::fmt::Display> std::fmt::Display for LiteralOr<'a, T> {
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 {
Self::Literal(l) => write!(f, "{l}"), Self::Literal(l) => write!(f, "{l}"),
@@ -27,7 +27,7 @@ impl<T: std::fmt::Display> std::fmt::Display for LiteralOr<T> {
} }
} }
impl std::fmt::Display for Literal { impl<'a> std::fmt::Display for Literal<'a> {
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 {
Literal::Number(n) => write!(f, "{}", n), Literal::Number(n) => write!(f, "{}", n),
@@ -38,16 +38,16 @@ impl std::fmt::Display for Literal {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum BinaryExpression { pub enum BinaryExpression<'a> {
Add(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Add(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
Multiply(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Multiply(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
Divide(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Divide(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
Subtract(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Subtract(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
Exponent(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Exponent(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
Modulo(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Modulo(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
} }
impl std::fmt::Display for BinaryExpression { impl<'a> std::fmt::Display for BinaryExpression<'a> {
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 {
BinaryExpression::Add(l, r) => write!(f, "({} + {})", l, r), BinaryExpression::Add(l, r) => write!(f, "({} + {})", l, r),
@@ -61,19 +61,19 @@ impl std::fmt::Display for BinaryExpression {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum LogicalExpression { pub enum LogicalExpression<'a> {
And(Box<Spanned<Expression>>, Box<Spanned<Expression>>), And(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
Or(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Or(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
Not(Box<Spanned<Expression>>), Not(Box<Spanned<Expression<'a>>>),
Equal(Box<Spanned<Expression>>, Box<Spanned<Expression>>), Equal(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
NotEqual(Box<Spanned<Expression>>, Box<Spanned<Expression>>), NotEqual(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
GreaterThan(Box<Spanned<Expression>>, Box<Spanned<Expression>>), GreaterThan(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
GreaterThanOrEqual(Box<Spanned<Expression>>, Box<Spanned<Expression>>), GreaterThanOrEqual(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
LessThan(Box<Spanned<Expression>>, Box<Spanned<Expression>>), LessThan(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
LessThanOrEqual(Box<Spanned<Expression>>, Box<Spanned<Expression>>), LessThanOrEqual(Box<Spanned<Expression<'a>>>, Box<Spanned<Expression<'a>>>),
} }
impl std::fmt::Display for LogicalExpression { impl<'a> std::fmt::Display for LogicalExpression<'a> {
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 {
LogicalExpression::And(l, r) => write!(f, "({} && {})", l, r), LogicalExpression::And(l, r) => write!(f, "({} && {})", l, r),
@@ -90,25 +90,25 @@ impl std::fmt::Display for LogicalExpression {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct AssignmentExpression { pub struct AssignmentExpression<'a> {
pub assignee: Box<Spanned<Expression>>, pub assignee: Box<Spanned<Expression<'a>>>,
pub expression: Box<Spanned<Expression>>, pub expression: Box<Spanned<Expression<'a>>>,
} }
impl std::fmt::Display for AssignmentExpression { impl<'a> std::fmt::Display for AssignmentExpression<'a> {
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.assignee, self.expression) write!(f, "({} = {})", self.assignee, self.expression)
} }
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct FunctionExpression { pub struct FunctionExpression<'a> {
pub name: Spanned<String>, pub name: Spanned<Cow<'a, str>>,
pub arguments: Vec<Spanned<String>>, pub arguments: Vec<Spanned<Cow<'a, str>>>,
pub body: BlockExpression, pub body: BlockExpression<'a>,
} }
impl std::fmt::Display for FunctionExpression { impl<'a> std::fmt::Display for FunctionExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
@@ -125,9 +125,9 @@ impl std::fmt::Display for FunctionExpression {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct BlockExpression(pub Vec<Spanned<Expression>>); pub struct BlockExpression<'a>(pub Vec<Spanned<Expression<'a>>>);
impl std::fmt::Display for BlockExpression { impl<'a> std::fmt::Display for BlockExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
@@ -142,12 +142,12 @@ impl std::fmt::Display for BlockExpression {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct InvocationExpression { pub struct InvocationExpression<'a> {
pub name: Spanned<String>, pub name: Spanned<Cow<'a, str>>,
pub arguments: Vec<Spanned<Expression>>, pub arguments: Vec<Spanned<Expression<'a>>>,
} }
impl std::fmt::Display for InvocationExpression { impl<'a> std::fmt::Display for InvocationExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
@@ -163,25 +163,25 @@ impl std::fmt::Display for InvocationExpression {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct MemberAccessExpression { pub struct MemberAccessExpression<'a> {
pub object: Box<Spanned<Expression>>, pub object: Box<Spanned<Expression<'a>>>,
pub member: Spanned<String>, pub member: Spanned<Cow<'a, str>>,
} }
impl std::fmt::Display for MemberAccessExpression { impl<'a> std::fmt::Display for MemberAccessExpression<'a> {
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.object, self.member) write!(f, "{}.{}", self.object, self.member)
} }
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct MethodCallExpression { pub struct MethodCallExpression<'a> {
pub object: Box<Spanned<Expression>>, pub object: Box<Spanned<Expression<'a>>>,
pub method: Spanned<String>, pub method: Spanned<Cow<'a, str>>,
pub arguments: Vec<Spanned<Expression>>, pub arguments: Vec<Spanned<Expression<'a>>>,
} }
impl std::fmt::Display for MethodCallExpression { impl<'a> std::fmt::Display for MethodCallExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
f, f,
@@ -198,12 +198,12 @@ impl std::fmt::Display for MethodCallExpression {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum LiteralOrVariable { pub enum LiteralOrVariable<'a> {
Literal(Literal), Literal(Literal<'a>),
Variable(Spanned<String>), Variable(Spanned<Cow<'a, str>>),
} }
impl std::fmt::Display for LiteralOrVariable { impl<'a> std::fmt::Display for LiteralOrVariable<'a> {
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 {
LiteralOrVariable::Literal(l) => write!(f, "{}", l), LiteralOrVariable::Literal(l) => write!(f, "{}", l),
@@ -213,46 +213,46 @@ impl std::fmt::Display for LiteralOrVariable {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct ConstDeclarationExpression { pub struct ConstDeclarationExpression<'a> {
pub name: Spanned<String>, pub name: Spanned<Cow<'a, str>>,
pub value: LiteralOr<SysCall>, pub value: LiteralOr<'a, SysCall<'a>>,
} }
impl ConstDeclarationExpression { impl<'a> ConstDeclarationExpression<'a> {
pub fn is_syscall_supported(call: &SysCall) -> bool { pub fn is_syscall_supported(call: &SysCall) -> bool {
use sys_call::System; use sys_call::System;
matches!(call, SysCall::System(sys) if matches!(sys, System::Hash(_))) matches!(call, SysCall::System(sys) if matches!(sys, System::Hash(_)))
} }
} }
impl std::fmt::Display for ConstDeclarationExpression { impl<'a> std::fmt::Display for ConstDeclarationExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(const {} = {})", self.name, self.value) write!(f, "(const {} = {})", self.name, self.value)
} }
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct DeviceDeclarationExpression { pub struct DeviceDeclarationExpression<'a> {
/// any variable-like name /// any variable-like name
pub name: Spanned<String>, pub name: Spanned<Cow<'a, str>>,
/// The device port, ex. (db, d0, d1, d2, d3, d4, d5) /// The device port, ex. (db, d0, d1, d2, d3, d4, d5)
pub device: String, pub device: Cow<'a, str>,
} }
impl std::fmt::Display for DeviceDeclarationExpression { impl<'a> std::fmt::Display for DeviceDeclarationExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(device {} = {})", self.name, self.device) write!(f, "(device {} = {})", self.name, self.device)
} }
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct IfExpression { pub struct IfExpression<'a> {
pub condition: Box<Spanned<Expression>>, pub condition: Box<Spanned<Expression<'a>>>,
pub body: Spanned<BlockExpression>, pub body: Spanned<BlockExpression<'a>>,
pub else_branch: Option<Box<Spanned<Expression>>>, pub else_branch: Option<Box<Spanned<Expression<'a>>>>,
} }
impl std::fmt::Display for IfExpression { impl<'a> std::fmt::Display for IfExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(if ({}) {}", self.condition, self.body)?; write!(f, "(if ({}) {}", self.condition, self.body)?;
if let Some(else_branch) = &self.else_branch { if let Some(else_branch) = &self.else_branch {
@@ -263,66 +263,45 @@ impl std::fmt::Display for IfExpression {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct LoopExpression { pub struct LoopExpression<'a> {
pub body: Spanned<BlockExpression>, pub body: Spanned<BlockExpression<'a>>,
} }
impl std::fmt::Display for LoopExpression { impl<'a> std::fmt::Display for LoopExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(loop {})", self.body) write!(f, "(loop {})", self.body)
} }
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct WhileExpression { pub struct WhileExpression<'a> {
pub condition: Box<Spanned<Expression>>, pub condition: Box<Spanned<Expression<'a>>>,
pub body: BlockExpression, pub body: BlockExpression<'a>,
} }
impl std::fmt::Display for WhileExpression { #[derive(Debug, PartialEq, Eq)]
pub struct TernaryExpression<'a> {
pub condition: Box<Spanned<Expression<'a>>>,
pub true_value: Box<Spanned<Expression<'a>>>,
pub false_value: Box<Spanned<Expression<'a>>>,
}
impl<'a> std::fmt::Display for TernaryExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"({} ? {} : {})",
self.condition, self.true_value, self.false_value
)
}
}
impl<'a> std::fmt::Display for WhileExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "(while {} {})", self.condition, self.body) write!(f, "(while {} {})", self.condition, self.body)
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
}
impl From<Span> for lsp_types::Range {
fn from(value: Span) -> Self {
Self {
start: lsp_types::Position {
line: value.start_line as u32,
character: value.start_col as u32,
},
end: lsp_types::Position {
line: value.end_line as u32,
character: value.end_col as u32,
},
}
}
}
impl From<&Span> for lsp_types::Range {
fn from(value: &Span) -> Self {
Self {
start: lsp_types::Position {
line: value.start_line as u32,
character: value.start_col as u32,
},
end: lsp_types::Position {
line: value.end_line as u32,
character: value.end_col as u32,
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Spanned<T> { pub struct Spanned<T> {
pub span: Span, pub span: Span,
@@ -347,32 +326,33 @@ impl<T> Deref for Spanned<T> {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Expression { pub enum Expression<'a> {
Assignment(Spanned<AssignmentExpression>), Assignment(Spanned<AssignmentExpression<'a>>),
Binary(Spanned<BinaryExpression>), Binary(Spanned<BinaryExpression<'a>>),
Block(Spanned<BlockExpression>), Block(Spanned<BlockExpression<'a>>),
Break(Span), Break(Span),
ConstDeclaration(Spanned<ConstDeclarationExpression>), ConstDeclaration(Spanned<ConstDeclarationExpression<'a>>),
Continue(Span), Continue(Span),
Declaration(Spanned<String>, Box<Spanned<Expression>>), Declaration(Spanned<Cow<'a, str>>, Box<Spanned<Expression<'a>>>),
DeviceDeclaration(Spanned<DeviceDeclarationExpression>), DeviceDeclaration(Spanned<DeviceDeclarationExpression<'a>>),
Function(Spanned<FunctionExpression>), Function(Spanned<FunctionExpression<'a>>),
If(Spanned<IfExpression>), If(Spanned<IfExpression<'a>>),
Invocation(Spanned<InvocationExpression>), Invocation(Spanned<InvocationExpression<'a>>),
Literal(Spanned<Literal>), Literal(Spanned<Literal<'a>>),
Logical(Spanned<LogicalExpression>), Logical(Spanned<LogicalExpression<'a>>),
Loop(Spanned<LoopExpression>), Loop(Spanned<LoopExpression<'a>>),
MemberAccess(Spanned<MemberAccessExpression>), MemberAccess(Spanned<MemberAccessExpression<'a>>),
MethodCall(Spanned<MethodCallExpression>), MethodCall(Spanned<MethodCallExpression<'a>>),
Negation(Box<Spanned<Expression>>), Negation(Box<Spanned<Expression<'a>>>),
Priority(Box<Spanned<Expression>>), Priority(Box<Spanned<Expression<'a>>>),
Return(Box<Spanned<Expression>>), Return(Option<Box<Spanned<Expression<'a>>>>),
Syscall(Spanned<SysCall>), Syscall(Spanned<SysCall<'a>>),
Variable(Spanned<String>), Ternary(Spanned<TernaryExpression<'a>>),
While(Spanned<WhileExpression>), Variable(Spanned<Cow<'a, str>>),
While(Spanned<WhileExpression<'a>>),
} }
impl std::fmt::Display for Expression { impl<'a> std::fmt::Display for Expression<'a> {
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 {
Expression::Assignment(e) => write!(f, "{}", e), Expression::Assignment(e) => write!(f, "{}", e),
@@ -393,8 +373,17 @@ impl std::fmt::Display for Expression {
Expression::MethodCall(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 {})",
if let Some(e) = e {
e.to_string()
} else {
"".to_string()
}
),
Expression::Syscall(e) => write!(f, "{}", e), Expression::Syscall(e) => write!(f, "{}", e),
Expression::Ternary(e) => write!(f, "{}", e),
Expression::Variable(id) => write!(f, "{}", id), Expression::Variable(id) => write!(f, "{}", id),
Expression::While(e) => write!(f, "{}", e), Expression::While(e) => write!(f, "{}", e),
} }

View File

@@ -5,9 +5,10 @@ edition = "2024"
[dependencies] [dependencies]
rust_decimal = { workspace = true } rust_decimal = { workspace = true }
quick-error = { workspace = true }
lsp-types = { workspace = true } lsp-types = { workspace = true }
thiserror = { workspace = true }
helpers = { path = "../helpers" } helpers = { path = "../helpers" }
logos = "0.16"
[dev-dependencies] [dev-dependencies]
anyhow = { version = "^1" } anyhow = { version = "^1" }

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,59 @@
use std::borrow::Cow;
use helpers::prelude::*; use helpers::prelude::*;
use logos::{Lexer, Logos, Skip, Span};
use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
use rust_decimal::Decimal; use rust_decimal::Decimal;
use thiserror::Error;
#[derive(Debug, Error, Default, Clone, PartialEq)]
pub enum LexError {
#[error("Attempted to parse an invalid number: {2}")]
NumberParse(usize, Span, String),
#[error("An invalid character was found in token stream: {2}")]
InvalidInput(usize, Span, String),
#[default]
#[error("An unknown error occurred")]
Other,
}
impl From<LexError> for Diagnostic {
fn from(value: LexError) -> Self {
match value {
LexError::NumberParse(line, col, str) | LexError::InvalidInput(line, col, str) => {
Diagnostic {
range: Range {
start: Position {
character: col.start as u32,
line: line as u32,
},
end: Position {
line: line as u32,
character: col.end as u32,
},
},
severity: Some(DiagnosticSeverity::ERROR),
message: str,
..Default::default()
}
}
_ => Diagnostic::default(),
}
}
}
impl LexError {
pub fn from_lexer<'a>(lex: &mut Lexer<'a, TokenType<'a>>) -> Self {
let mut span = lex.span();
let line = lex.extras.line_count;
span.start -= lex.extras.line_start_index;
span.end -= lex.extras.line_start_index;
Self::InvalidInput(line, span, lex.slice().chars().as_str().to_string())
}
}
// Define a local macro to consume the list // Define a local macro to consume the list
macro_rules! generate_check { macro_rules! generate_check {
@@ -10,29 +64,40 @@ macro_rules! generate_check {
} }
} }
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Default)]
pub struct Token { pub struct Extras {
/// The type of the token pub line_count: usize,
pub token_type: TokenType, pub line_start_index: usize,
/// The line where the token was found
pub line: usize,
/// The column where the token was found
pub column: usize,
pub original_string: Option<String>,
} }
impl Token { fn update_line_index<'a>(lex: &mut Lexer<'a, TokenType<'a>>) -> Skip {
pub fn new( lex.extras.line_count += 1;
token_type: TokenType, lex.extras.line_start_index = lex.span().end;
line: usize, Skip
column: usize, }
original: Option<String>,
) -> Self { #[derive(Debug, PartialEq, Eq, Clone)]
pub struct Token<'a> {
/// The type of the token
pub token_type: TokenType<'a>,
/// The line where the token was found
pub line: usize,
/// The span where the token starts and ends
pub span: Span,
}
impl<'a> std::fmt::Display for Token<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.token_type)
}
}
impl<'a> Token<'a> {
pub fn new(token_type: TokenType<'a>, line: usize, span: Span) -> Self {
Self { Self {
token_type, token_type,
line, line,
column, span,
original_string: original,
} }
} }
} }
@@ -79,25 +144,187 @@ impl Temperature {
} }
} }
#[derive(Debug, PartialEq, Hash, Eq, Clone)] macro_rules! symbol {
pub enum TokenType { ($var:ident) => {
|_| Symbol::$var
};
}
macro_rules! keyword {
($var:ident) => {
|_| Keyword::$var
};
}
#[derive(Debug, PartialEq, Hash, Eq, Clone, Logos)]
#[logos(skip r"[ \t\f]+")]
#[logos(extras = Extras)]
#[logos(error(LexError, LexError::from_lexer))]
pub enum TokenType<'a> {
#[regex(r"\n", update_line_index)]
Newline,
// matches strings with double quotes
#[regex(r#""(?:[^"\\]|\\.)*""#, |v| {
let str = v.slice();
Cow::from(&str[1..str.len() - 1])
})]
// matches strings with single quotes
#[regex(r#"'(?:[^'\\]|\\.)*'"#, |v| {
let str = v.slice();
Cow::from(&str[1..str.len() - 1])
})]
/// Represents a string token /// Represents a string token
String(String), String(Cow<'a, str>),
#[regex(r"[0-9][0-9_]*(\.[0-9][0-9_]*)?([cfk])?", parse_number)]
/// Represents a number token /// Represents a number token
Number(Number), Number(Number),
#[token("true", |_| true)]
#[token("false", |_| false)]
/// Represents a boolean token /// Represents a boolean token
Boolean(bool), Boolean(bool),
#[token("continue", keyword!(Continue))]
#[token("const", keyword!(Const))]
#[token("let", keyword!(Let))]
#[token("fn", keyword!(Fn))]
#[token("if", keyword!(If))]
#[token("device", keyword!(Device))]
#[token("else", keyword!(Else))]
#[token("return", keyword!(Return))]
#[token("enum", keyword!(Enum))]
#[token("loop", keyword!(Loop))]
#[token("break", keyword!(Break))]
#[token("while", keyword!(While))]
/// Represents a keyword token /// Represents a keyword token
Keyword(Keyword), Keyword(Keyword),
#[regex(r"[a-zA-Z_][a-zA-Z0-9_]*", |v| Cow::from(v.slice()))]
/// Represents an identifier token /// Represents an identifier token
Identifier(String), Identifier(Cow<'a, str>),
#[token("(", symbol!(LParen))]
#[token(")", symbol!(RParen))]
#[token("{", symbol!(LBrace))]
#[token("}", symbol!(RBrace))]
#[token("[", symbol!(LBracket))]
#[token("]", symbol!(RBracket))]
#[token(";", symbol!(Semicolon))]
#[token(":", symbol!(Colon))]
#[token(",", symbol!(Comma))]
#[token("+", symbol!(Plus))]
#[token("-", symbol!(Minus))]
#[token("*", symbol!(Asterisk))]
#[token("/", symbol!(Slash))]
#[token("<", symbol!(LessThan))]
#[token(">", symbol!(GreaterThan))]
#[token("=", symbol!(Assign))]
#[token("!", symbol!(LogicalNot))]
#[token(".", symbol!(Dot))]
#[token("^", symbol!(Caret))]
#[token("%", symbol!(Percent))]
#[token("?", symbol!(Question))]
#[token("==", symbol!(Equal))]
#[token("!=", symbol!(NotEqual))]
#[token("&&", symbol!(LogicalAnd))]
#[token("||", symbol!(LogicalOr))]
#[token("<=", symbol!(LessThanOrEqual))]
#[token(">=", symbol!(GreaterThanOrEqual))]
#[token("**", symbol!(Exp))]
/// Represents a symbol token /// Represents a symbol token
Symbol(Symbol), Symbol(Symbol),
#[token("//", |lex| Comment::Line(read_line(lex)))]
#[token("///", |lex| Comment::Doc(read_line(lex)))]
/// Represents a comment, both a line comment and a doc comment
Comment(Comment<'a>),
#[end]
/// Represents an end of file token /// Represents an end of file token
EOF, EOF,
} }
impl Documentation for TokenType { fn read_line<'a>(lexer: &mut Lexer<'a, TokenType<'a>>) -> Cow<'a, str> {
let rem = lexer.remainder();
let len = rem.find('\n').unwrap_or(rem.len());
let content = rem[..len].trim().to_string();
lexer.bump(len);
Cow::from(content)
}
#[derive(Hash, Debug, Eq, PartialEq, Clone)]
pub enum Comment<'a> {
Line(Cow<'a, str>),
Doc(Cow<'a, str>),
}
fn parse_number<'a>(lexer: &mut Lexer<'a, TokenType<'a>>) -> Result<Number, LexError> {
let slice = lexer.slice();
let last_char = slice.chars().last().unwrap_or_default();
let (num_str, suffix) = match last_char {
'c' | 'k' | 'f' => (&slice[..slice.len() - 1], Some(last_char)),
_ => (slice, None),
};
let clean_str = if num_str.contains('_') {
num_str.replace('_', "")
} else {
num_str.to_string()
};
let line = lexer.extras.line_count;
let mut span = lexer.span();
span.end -= lexer.extras.line_start_index;
span.start -= lexer.extras.line_start_index;
let num = if clean_str.contains('.') {
Number::Decimal(
clean_str
.parse::<Decimal>()
.map_err(|_| LexError::NumberParse(line, span, slice.to_string()))?,
)
} else {
Number::Integer(
clean_str
.parse::<i128>()
.map_err(|_| LexError::NumberParse(line, span, slice.to_string()))?,
)
};
if let Some(suffix) = suffix {
Ok(match suffix {
'c' => Temperature::Celsius(num),
'f' => Temperature::Fahrenheit(num),
'k' => Temperature::Kelvin(num),
_ => unreachable!(),
}
.to_kelvin())
} else {
Ok(num)
}
}
impl<'a> std::fmt::Display for Comment<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Line(c) => write!(f, "// {}", c),
Self::Doc(d) => {
let lines = d
.split('\n')
.map(|s| format!("/// {s}"))
.collect::<Vec<_>>()
.join("\n");
write!(f, "{}", lines)
}
}
}
}
impl<'a> Documentation for TokenType<'a> {
fn docs(&self) -> String { fn docs(&self) -> String {
match self { match self {
Self::Keyword(k) => k.docs(), Self::Keyword(k) => k.docs(),
@@ -112,7 +339,7 @@ impl Documentation for TokenType {
helpers::with_syscalls!(generate_check); helpers::with_syscalls!(generate_check);
impl From<TokenType> for u32 { impl<'a> From<TokenType<'a>> for u32 {
fn from(value: TokenType) -> Self { fn from(value: TokenType) -> Self {
match value { match value {
TokenType::String(_) => 1, TokenType::String(_) => 1,
@@ -128,6 +355,7 @@ impl From<TokenType> for u32 {
| Keyword::Return => 4, | Keyword::Return => 4,
_ => 5, _ => 5,
}, },
TokenType::Comment(_) => 8,
TokenType::Identifier(s) => { TokenType::Identifier(s) => {
if is_syscall(&s) { if is_syscall(&s) {
10 10
@@ -146,12 +374,12 @@ impl From<TokenType> for u32 {
7 7
} }
} }
TokenType::EOF => 0, _ => 0,
} }
} }
} }
impl std::fmt::Display for TokenType { impl<'a> std::fmt::Display for TokenType<'a> {
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 {
TokenType::String(s) => write!(f, "{}", s), TokenType::String(s) => write!(f, "{}", s),
@@ -160,7 +388,9 @@ impl std::fmt::Display for TokenType {
TokenType::Keyword(k) => write!(f, "{:?}", k), TokenType::Keyword(k) => write!(f, "{:?}", k),
TokenType::Identifier(i) => write!(f, "{}", i), TokenType::Identifier(i) => write!(f, "{}", i),
TokenType::Symbol(s) => write!(f, "{}", s), TokenType::Symbol(s) => write!(f, "{}", s),
TokenType::Comment(c) => write!(f, "{}", c),
TokenType::EOF => write!(f, "EOF"), TokenType::EOF => write!(f, "EOF"),
_ => write!(f, ""),
} }
} }
} }
@@ -173,6 +403,12 @@ pub enum Number {
Decimal(Decimal), Decimal(Decimal),
} }
impl From<bool> for Number {
fn from(value: bool) -> Self {
Self::Integer(if value { 1 } else { 0 })
}
}
impl From<Number> for Decimal { impl From<Number> for Decimal {
fn from(value: Number) -> Self { fn from(value: Number) -> Self {
match value { match value {
@@ -306,6 +542,8 @@ pub enum Symbol {
Caret, Caret,
/// Represents the `%` symbol /// Represents the `%` symbol
Percent, Percent,
/// Represents the `?` symbol
Question,
// Double Character Symbols // Double Character Symbols
/// Represents the `==` symbol /// Represents the `==` symbol
@@ -372,6 +610,7 @@ impl std::fmt::Display for Symbol {
Self::Asterisk => write!(f, "*"), Self::Asterisk => write!(f, "*"),
Self::Slash => write!(f, "/"), Self::Slash => write!(f, "/"),
Self::LessThan => write!(f, "<"), Self::LessThan => write!(f, "<"),
Self::Question => write!(f, "?"),
Self::LessThanOrEqual => write!(f, "<="), Self::LessThanOrEqual => write!(f, "<="),
Self::GreaterThan => write!(f, ">"), Self::GreaterThan => write!(f, ">"),
Self::GreaterThanOrEqual => write!(f, ">="), Self::GreaterThanOrEqual => write!(f, ">="),

View File

@@ -1,5 +1,5 @@
use compiler::Compiler; use compiler::{CompilationResult, Compiler};
use helpers::Documentation; use helpers::{Documentation, Span};
use parser::{sys_call::SysCall, Parser}; use parser::{sys_call::SysCall, Parser};
use safer_ffi::prelude::*; use safer_ffi::prelude::*;
use std::io::BufWriter; use std::io::BufWriter;
@@ -8,6 +8,20 @@ use tokenizer::{
Tokenizer, Tokenizer,
}; };
#[derive_ReprC]
#[repr(C)]
pub struct FfiSourceMapEntry {
pub line_number: u32,
pub span: FfiRange,
}
#[derive_ReprC]
#[repr(C)]
pub struct FfiCompilationResult {
pub output_code: safer_ffi::String,
pub source_map: safer_ffi::Vec<FfiSourceMapEntry>,
}
#[derive_ReprC] #[derive_ReprC]
#[repr(C)] #[repr(C)]
pub struct FfiToken { pub struct FfiToken {
@@ -34,6 +48,17 @@ pub struct FfiDocumentedItem {
docs: safer_ffi::String, docs: safer_ffi::String,
} }
impl From<Span> for FfiRange {
fn from(value: Span) -> Self {
Self {
start_line: value.start_line as u32,
end_line: value.end_line as u32,
start_col: value.start_col as u32,
end_col: value.end_col as u32,
}
}
}
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 {
@@ -69,6 +94,11 @@ impl From<lsp_types::Diagnostic> for FfiDiagnostic {
} }
} }
#[ffi_export]
pub fn free_ffi_compilation_result(input: FfiCompilationResult) {
drop(input)
}
#[ffi_export] #[ffi_export]
pub fn free_ffi_token_vec(v: safer_ffi::Vec<FfiToken>) { pub fn free_ffi_token_vec(v: safer_ffi::Vec<FfiToken>) {
drop(v) drop(v)
@@ -94,33 +124,63 @@ pub fn free_docs_vec(v: safer_ffi::Vec<FfiDocumentedItem>) {
/// 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
/// from the GC from a `GetBytes()` call on a string in C#. /// from the GC from a `GetBytes()` call on a string in C#.
#[ffi_export] #[ffi_export]
pub fn compile_from_string(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::String { pub fn compile_from_string(input: safer_ffi::slice::Ref<'_, u16>) -> FfiCompilationResult {
let res = std::panic::catch_unwind(|| { let res = std::panic::catch_unwind(|| {
let mut writer = BufWriter::new(Vec::new()); let input = String::from_utf16_lossy(input.as_slice());
let tokenizer = Tokenizer::from(String::from_utf16_lossy(input.as_slice())); let tokenizer = Tokenizer::from(input.as_str());
let parser = Parser::new(tokenizer); let parser = Parser::new(tokenizer);
let compiler = Compiler::new(parser, &mut writer, None); let compiler = Compiler::new(parser, None);
if !compiler.compile().is_empty() { let res = compiler.compile();
return safer_ffi::String::EMPTY;
if !res.errors.is_empty() {
return (safer_ffi::String::EMPTY, res.instructions.source_map());
} }
let mut writer = BufWriter::new(Vec::new());
// writing into a Vec<u8>. This should not fail.
let optimized = optimizer::optimize(res.instructions);
let map = optimized.source_map();
_ = optimized.write(&mut writer);
let Ok(compiled_vec) = writer.into_inner() else { let Ok(compiled_vec) = writer.into_inner() else {
return safer_ffi::String::EMPTY; return (safer_ffi::String::EMPTY, map);
}; };
// Safety: I know the compiler only outputs valid utf8 // Safety: I know the compiler only outputs valid utf8
safer_ffi::String::from(unsafe { String::from_utf8_unchecked(compiled_vec) }) (
safer_ffi::String::from(unsafe { String::from_utf8_unchecked(compiled_vec) }),
map,
)
}); });
res.unwrap_or("".into()) if let Ok((res_str, source_map)) = res {
FfiCompilationResult {
source_map: source_map
.into_iter()
.map(|(line_num, span)| FfiSourceMapEntry {
span: span.into(),
line_number: line_num as u32,
})
.collect::<Vec<_>>()
.into(),
output_code: res_str,
}
} else {
FfiCompilationResult {
output_code: "".into(),
source_map: vec![].into(),
}
}
} }
#[ffi_export] #[ffi_export]
pub fn tokenize_line(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<FfiToken> { pub fn tokenize_line(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<FfiToken> {
let res = std::panic::catch_unwind(|| { let res = std::panic::catch_unwind(|| {
let tokenizer = Tokenizer::from(String::from_utf16_lossy(input.as_slice())); let input = String::from_utf16_lossy(input.as_slice());
let tokenizer = Tokenizer::from(input.as_str());
let mut tokens = Vec::new(); let mut tokens = Vec::new();
@@ -136,34 +196,31 @@ pub fn tokenize_line(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<Ff
} }
match token { match token {
Err(ref e) => { Err(ref e) => {
use tokenizer::token::LexError;
use tokenizer::Error::*; use tokenizer::Error::*;
let (err_str, col, og) = match e { let (err_str, _, span) = match e {
NumberParseError(_, _, col, og) LexError(LexError::NumberParse(line, span, err))
| DecimalParseError(_, _, col, og) | LexError(LexError::InvalidInput(line, span, err)) => {
| UnknownSymbolError(_, _, col, og) (err.to_string(), line, span)
| UnknownKeywordOrIdentifierError(_, _, col, og) => {
(e.to_string(), col, og)
} }
_ => continue, _ => continue,
}; };
tokens.push(FfiToken { tokens.push(FfiToken {
column: *col as i32, column: span.start as i32,
error: err_str.into(), error: err_str.into(),
tooltip: "".into(), tooltip: "".into(),
length: og.len() as i32, length: (span.end - span.start) as i32,
token_kind: 0, token_kind: 0,
}) })
} }
Ok(Token { Ok(Token {
column, span, token_type, ..
original_string,
token_type,
..
}) => tokens.push(FfiToken { }) => tokens.push(FfiToken {
column: column as i32, column: span.start as i32,
error: "".into(), error: "".into(),
length: (original_string.unwrap_or_default().len()) as i32, length: (span.end - span.start) as i32,
tooltip: token_type.docs().into(), tooltip: token_type.docs().into(),
token_kind: token_type.into(), token_kind: token_type.into(),
}), }),
@@ -179,11 +236,14 @@ pub fn tokenize_line(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<Ff
#[ffi_export] #[ffi_export]
pub fn diagnose_source(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<FfiDiagnostic> { pub fn diagnose_source(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<FfiDiagnostic> {
let res = std::panic::catch_unwind(|| { let res = std::panic::catch_unwind(|| {
let mut writer = BufWriter::new(Vec::new()); let input = String::from_utf16_lossy(input.as_slice());
let tokenizer = Tokenizer::from(String::from_utf16_lossy(input.as_slice()));
let compiler = Compiler::new(Parser::new(tokenizer), &mut writer, None);
let diagnosis = compiler.compile(); let tokenizer = Tokenizer::from(input.as_str());
let compiler = Compiler::new(Parser::new(tokenizer), None);
let CompilationResult {
errors: diagnosis, ..
} = compiler.compile();
let mut result_vec: Vec<FfiDiagnostic> = Vec::with_capacity(diagnosis.len()); let mut result_vec: Vec<FfiDiagnostic> = Vec::with_capacity(diagnosis.len());

View File

@@ -1,37 +1,46 @@
#![allow(clippy::result_large_err)] #![allow(clippy::result_large_err)]
#[macro_use]
extern crate quick_error;
use clap::Parser; use clap::Parser;
use compiler::Compiler; use compiler::{CompilationResult, Compiler};
use parser::Parser as ASTParser; use parser::Parser as ASTParser;
use std::{ use std::{
fs::File, fs::File,
io::{stderr, BufWriter, Read, Write}, io::{stderr, BufWriter, Read, Write},
path::PathBuf, path::PathBuf,
}; };
use thiserror::Error;
use tokenizer::{self, Tokenizer}; use tokenizer::{self, Tokenizer};
quick_error! { #[derive(Error, Debug)]
#[derive(Debug)] enum Error<'a> {
enum StationlangError { #[error(transparent)]
TokenizerError(err: tokenizer::Error) { Tokenizer(tokenizer::Error),
from()
display("Tokenizer error: {}", err) #[error(transparent)]
} Parser(parser::Error<'a>),
ParserError(err: parser::Error) {
from() #[error(transparent)]
display("Parser error: {}", err) Compile(compiler::Error<'a>),
}
CompileError(err: compiler::Error) { #[error(transparent)]
from() IO(#[from] std::io::Error),
display("Compile error: {}", err) }
}
IoError(err: std::io::Error) { impl<'a> From<parser::Error<'a>> for Error<'a> {
from() fn from(value: parser::Error<'a>) -> Self {
display("IO error: {}", err) Self::Parser(value)
} }
}
impl<'a> From<compiler::Error<'a>> for Error<'a> {
fn from(value: compiler::Error<'a>) -> Self {
Self::Compile(value)
}
}
impl<'a> From<tokenizer::Error> for Error<'a> {
fn from(value: tokenizer::Error) -> Self {
Self::Tokenizer(value)
} }
} }
@@ -46,12 +55,17 @@ struct Args {
output_file: Option<PathBuf>, output_file: Option<PathBuf>,
} }
fn run_logic() -> Result<(), StationlangError> { fn run_logic<'a>() -> Result<(), Error<'a>> {
let args = Args::parse(); let args = Args::parse();
let input_file = args.input_file; let input_file = args.input_file;
let tokenizer: Tokenizer = match input_file { let input_string = match input_file {
Some(input_file) => Tokenizer::from_path(&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();
buf
}
None => { None => {
let mut buf = String::new(); let mut buf = String::new();
let stdin = std::io::stdin(); let stdin = std::io::stdin();
@@ -62,10 +76,11 @@ fn run_logic() -> Result<(), StationlangError> {
return Ok(()); return Ok(());
} }
Tokenizer::from(buf) buf
} }
}; };
let tokenizer = Tokenizer::from(input_string.as_str());
let parser = ASTParser::new(tokenizer); let parser = ASTParser::new(tokenizer);
let mut writer: BufWriter<Box<dyn Write>> = match args.output_file { let mut writer: BufWriter<Box<dyn Write>> = match args.output_file {
@@ -73,30 +88,33 @@ fn run_logic() -> Result<(), StationlangError> {
None => BufWriter::new(Box::new(std::io::stdout())), None => BufWriter::new(Box::new(std::io::stdout())),
}; };
let compiler = Compiler::new(parser, &mut writer, None); let compiler = Compiler::new(parser, None);
let mut errors = compiler.compile(); let CompilationResult {
errors,
instructions,
..
} = compiler.compile();
if !errors.is_empty() { if !errors.is_empty() {
let mut std_error = stderr(); let mut std_error = stderr();
let last = errors.pop(); let errors = errors.into_iter().map(Error::from);
let errors = errors.into_iter().map(StationlangError::from);
std_error.write_all(b"Compilation error:\n")?; std_error.write_all(b"Compilation error:\n")?;
for err in errors { for err in errors {
std_error.write_all(format!("{}\n", err).as_bytes())?; std_error.write_all(format!("{}\n", err).as_bytes())?;
} }
return Err(StationlangError::from(last.unwrap()));
} }
optimizer::optimize(instructions).write(&mut writer)?;
writer.flush()?; writer.flush()?;
Ok(()) Ok(())
} }
fn main() -> Result<(), StationlangError> { fn main() -> anyhow::Result<()> {
run_logic()?; run_logic()?;
Ok(()) Ok(())

43
spilling.slang Normal file
View File

@@ -0,0 +1,43 @@
device self = "db";
device gasSensor = "d0";
device atmosAnal = "d1";
device atmosValve = "d2";
device atmosTank = "d3";
device atmosInlet = "d4";
atmosInlet.Lock = true;
atmosInlet.Mode = 1;
atmosValve.On = false;
atmosValve.Lock = true;
let isPumping = false;
let tempPressure = 0;
loop {
yield();
let temp = gasSensor.Temperature;
let pres = atmosAnal.Pressure;
let liqV = atmosAnal.VolumeOfLiquid;
let tempVol = atmosAnal.Volume;
let stress = 5_000 * liqV / tempVol;
tempPressure = isPumping ? 1_000 : 10_000;
let shouldTurnOnInlet = (
temp > 0c &&
pres < tempPressure &&
stress < 50
);
isPumping = (
!shouldTurnOnInlet &&
atmosTank.Pressure < 35_000 &&
atmosAnal.RatioPollutant == 0 &&
atmosAnal.RatioLiquidPollutant == 0 &&
atmosAnal.Pressure > 1_000
);
atmosValve.On = isPumping;
atmosInlet.On = shouldTurnOnInlet;
}

72
test.slang Normal file
View File

@@ -0,0 +1,72 @@
/// Laree script V1
device self = "db";
device larre = "d0";
device exportChute = "d1";
const TOTAL_SLOTS = 19;
const EXPORT_CHUTE = 1;
const START_STATION = 2;
let currentIndex = 0;
/// Waits for the larre to be idle before continuing
fn waitForIdle() {
yield();
while (!larre.Idle) {
yield();
}
}
/// Instructs the Larre to go to the chute and deposit
/// what is currently in its arm
fn deposit() {
larre.Setting = EXPORT_CHUTE;
waitForIdle();
larre.Activate = true;
waitForIdle();
exportChute.Open = false;
}
/// This function is responsible for checking the plant under
/// the larre at this index, and harvesting if applicable
fn checkAndHarvest(currentIndex) {
if (currentIndex <= EXPORT_CHUTE || ls(larre, 255, "Seeding") < 1) {
return;
}
// harvest from this device
while (ls(larre, 255, "Mature")) {
yield();
larre.Activate = true;
}
let hasRemainingPlant = ls(larre, 255, "Occupied");
// move to the export chute
larre.Setting = EXPORT_CHUTE;
waitForIdle();
deposit();
if (hasRemainingPlant) {
deposit();
}
larre.Setting = currentIndex;
waitForIdle();
if (ls(larre, 0, "Occupied")) {
larre.Activate = true;
}
waitForIdle();
}
loop {
yield();
if (!larre.Idle) {
continue;
}
let newIndex = currentIndex + 1 > TOTAL_SLOTS ? START_STATION : currentIndex + 1;
checkAndHarvest(currentIndex);
larre.Setting = newIndex;
currentIndex = newIndex;
}