WIP -- emit compilation errors

This commit is contained in:
2025-12-01 02:54:53 -07:00
parent 06a151ab7e
commit 25d9222bd4
8 changed files with 395 additions and 178 deletions

View File

@@ -1,6 +1,7 @@
namespace Slang;
using System;
using System.Collections.Generic;
using System.Text;
using StationeersIC10Editor;
@@ -53,7 +54,7 @@ public static unsafe class SlangExtensions
var color = GetColorForKind(token.token_kind);
int colIndex = token.column;
int colIndex = token.column - 1;
if (colIndex < 0)
colIndex = 0;
@@ -80,20 +81,50 @@ public static unsafe class SlangExtensions
return list;
}
public static unsafe List<Diagnostic> ToList(this Vec_FfiDiagnostic_t vec)
{
var toReturn = new List<Diagnostic>((int)vec.len);
var currentPtr = vec.ptr;
for (int i = 0; i < (int)vec.len; i++)
{
var item = currentPtr[i];
toReturn.Add(
new Slang.Diagnostic
{
Message = item.message.AsString(),
Severity = item.severity,
Range = new Slang.Range
{
EndCol = item.range.end_col - 1,
EndLine = item.range.end_line - 1,
StartCol = item.range.start_col - 1,
StartLine = item.range.end_line - 1,
},
}
);
}
Ffi.free_ffi_diagnostic_vec(vec);
return toReturn;
}
private static uint GetColorForKind(uint kind)
{
switch (kind)
{
case 1:
return SlangFormatter.ColorInstruction; // Keyword
case 2:
return SlangFormatter.ColorDefault; // Identifier
case 3:
return SlangFormatter.ColorNumber; // Number
case 4:
return SlangFormatter.ColorString; // String
case 5:
case 2:
return SlangFormatter.ColorString; // Number
case 3:
return SlangFormatter.ColorInstruction; // Boolean
case 4:
return SlangFormatter.ColorInstruction; // Keyword
case 5:
return SlangFormatter.ColorInstruction; // Identifier
case 6:
return SlangFormatter.ColorDefault; // Symbol
default:

View File

@@ -15,13 +15,11 @@
#pragma warning disable SA1500, SA1505, SA1507,
#pragma warning disable SA1600, SA1601, SA1604, SA1605, SA1611, SA1615, SA1649,
namespace Slang
{
namespace Slang {
using System;
using System.Runtime.InteropServices;
public unsafe partial class Ffi
{
public unsafe partial class Ffi {
#if IOS
private const string RustLib = "slang.framework/slang";
#else
@@ -49,13 +47,11 @@ namespace Slang
/// use the <c>Option< slice_ptr<_> ></c> type.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 16)]
public unsafe struct slice_ref_uint16_t
{
public unsafe struct slice_ref_uint16_t {
/// <summary>
/// Pointer to the first element (if any).
/// </summary>
public UInt16 /*const*/
* ptr;
public UInt16 /*const*/ * ptr;
/// <summary>
/// Element count
@@ -67,8 +63,7 @@ namespace Slang
/// 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_uint8_t
{
public unsafe struct Vec_uint8_t {
public byte * ptr;
public UIntPtr len;
@@ -76,21 +71,20 @@ namespace Slang
public UIntPtr cap;
}
public unsafe partial class Ffi
{
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 extern unsafe Vec_uint8_t compile_from_string(slice_ref_uint16_t input);
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
Vec_uint8_t compile_from_string (
slice_ref_uint16_t input);
}
[StructLayout(LayoutKind.Sequential, Size = 16)]
public unsafe struct FfiRange_t
{
public unsafe struct FfiRange_t {
public UInt32 start_col;
public UInt32 end_col;
@@ -101,8 +95,7 @@ namespace Slang
}
[StructLayout(LayoutKind.Sequential, Size = 48)]
public unsafe struct FfiDiagnostic_t
{
public unsafe struct FfiDiagnostic_t {
public Vec_uint8_t message;
public Int32 severity;
@@ -114,8 +107,7 @@ namespace Slang
/// 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_FfiDiagnostic_t
{
public unsafe struct Vec_FfiDiagnostic_t {
public FfiDiagnostic_t * ptr;
public UIntPtr len;
@@ -123,21 +115,20 @@ namespace Slang
public UIntPtr cap;
}
public unsafe partial class Ffi
{
[DllImport(RustLib, ExactSpelling = true)]
public static extern unsafe Vec_FfiDiagnostic_t diagnose_source();
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
Vec_FfiDiagnostic_t diagnose_source (
slice_ref_uint16_t input);
}
public unsafe partial class Ffi
{
[DllImport(RustLib, ExactSpelling = true)]
public static extern unsafe void free_ffi_diagnostic_vec(Vec_FfiDiagnostic_t v);
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
void free_ffi_diagnostic_vec (
Vec_FfiDiagnostic_t v);
}
[StructLayout(LayoutKind.Sequential, Size = 64)]
public unsafe struct FfiToken_t
{
public unsafe struct FfiToken_t {
public Vec_uint8_t tooltip;
public Vec_uint8_t error;
@@ -153,8 +144,7 @@ namespace Slang
/// 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_FfiToken_t
{
public unsafe struct Vec_FfiToken_t {
public FfiToken_t * ptr;
public UIntPtr len;
@@ -162,15 +152,23 @@ namespace Slang
public UIntPtr cap;
}
public unsafe partial class Ffi
{
[DllImport(RustLib, ExactSpelling = true)]
public static extern unsafe void free_ffi_token_vec(Vec_FfiToken_t v);
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
void free_ffi_token_vec (
Vec_FfiToken_t v);
}
public unsafe partial class Ffi
{
[DllImport(RustLib, ExactSpelling = true)]
public static extern unsafe void free_string(Vec_uint8_t s);
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
void free_string (
Vec_uint8_t s);
}
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
Vec_FfiToken_t tokenize_line (
slice_ref_uint16_t input);
}
} /* Slang */

View File

@@ -1,40 +1,138 @@
namespace Slang;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using StationeersIC10Editor;
public class SlangFormatter : ICodeFormatter
{
private Timer _timer;
private System.Timers.Timer _timer;
private CancellationTokenSource? _lspCancellationToken;
private readonly SynchronizationContext? _mainThreadContext;
private volatile bool IsDiagnosing = false;
public static readonly uint ColorInstruction = ColorFromHTML("#ffff00");
public static readonly uint ColorString = ColorFromHTML("#ce9178");
private object _textLock = new();
public SlangFormatter()
{
_timer = new Timer(250);
// 1. Capture the Main Thread context.
// This works because the Editor instantiates this class on the main thread.
_mainThreadContext = SynchronizationContext.Current;
this.OnCodeChanged += HandleCodeChanged;
_timer = new System.Timers.Timer(250);
_timer.AutoReset = false;
}
public override string Compile()
{
L.Info("ICodeFormatter attempted to compile source code.");
return this.Lines.RawText;
}
public override Line ParseLine(string line)
{
return new Line(line);
HandleCodeChanged();
return Marshal.TokenizeLine(line);
}
private void HandleCodeChanged()
{
_timer.Stop();
_timer.Dispose();
_timer = new Timer(250);
_timer.Elapsed += (_, _) => HandleLsp();
if (IsDiagnosing)
return;
_lspCancellationToken?.Cancel();
_lspCancellationToken?.Dispose();
_lspCancellationToken = new CancellationTokenSource();
_ = HandleLsp(_lspCancellationToken.Token, this.RawText);
}
private void HandleLsp() { }
private void OnTimerElapsed(object sender, ElapsedEventArgs e) { }
private async Task HandleLsp(CancellationToken cancellationToken, string text)
{
try
{
await Task.Delay(500, cancellationToken);
if (cancellationToken.IsCancellationRequested)
return;
List<Diagnostic> diagnosis = Marshal.DiagnoseSource(text);
var dict = diagnosis
.GroupBy(d => d.Range.StartLine)
.ToDictionary(g => g.Key, g => g.ToList());
// 3. Dispatch the UI update to the Main Thread
if (_mainThreadContext != null)
{
// Post ensures ApplyDiagnostics runs on the captured thread (Main Thread)
_mainThreadContext.Post(_ => ApplyDiagnostics(dict), null);
}
else
{
// Fallback: If context is null (rare in Unity), try running directly
// but warn, as this might crash if not thread-safe.
L.Warning("SynchronizationContext was null. Attempting direct update (risky).");
ApplyDiagnostics(dict);
}
}
finally { }
}
// This runs on the Main Thread
private void ApplyDiagnostics(Dictionary<uint, List<Diagnostic>> dict)
{
IsDiagnosing = true;
// Standard LSP uses 0-based indexing.
for (int i = 0; i < this.Lines.Count; i++)
{
uint lineIndex = (uint)i;
if (dict.TryGetValue(lineIndex, out var lineDiagnostics))
{
var line = this.Lines[i];
if (line is null)
{
continue;
}
var tokenMap = line.Tokens.ToDictionary((t) => t.Column);
foreach (var diag in lineDiagnostics)
{
var newToken = new SemanticToken
{
Column = (int)diag.Range.StartCol,
Length = (int)(diag.Range.EndCol - diag.Range.StartCol),
Line = i,
IsError = true,
Data = diag.Message,
Color = ICodeFormatter.ColorError,
};
L.Info(
$"Col: {newToken.Column} -- Length: {newToken.Length} -- Msg: {newToken.Data}"
);
tokenMap[newToken.Column] = newToken;
}
line.ClearTokens();
foreach (var token in tokenMap.Values)
{
line.AddToken(token);
}
}
}
IsDiagnosing = false;
}
}

View File

@@ -1,11 +1,27 @@
namespace Slang;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using StationeersIC10Editor;
public struct Range
{
public uint StartCol;
public uint EndCol;
public uint StartLine;
public uint EndLine;
}
public struct Diagnostic
{
public string Message;
public int Severity;
public Range Range;
}
public static class Marshal
{
private static IntPtr _libraryHandle = IntPtr.Zero;
@@ -63,13 +79,7 @@ public static class Marshal
public static unsafe bool CompileFromString(string inputString, out string compiledString)
{
if (String.IsNullOrEmpty(inputString))
{
compiledString = String.Empty;
return false;
}
if (!EnsureLibLoaded())
if (String.IsNullOrEmpty(inputString) || !EnsureLibLoaded())
{
compiledString = String.Empty;
return false;
@@ -101,6 +111,46 @@ public static class Marshal
}
}
public static unsafe List<Diagnostic> DiagnoseSource(string inputString)
{
if (string.IsNullOrEmpty(inputString) || !EnsureLibLoaded())
{
return new();
}
fixed (char* ptrInput = inputString)
{
var input = new slice_ref_uint16_t
{
ptr = (ushort*)ptrInput,
len = (UIntPtr)inputString.Length,
};
return Ffi.diagnose_source(input).ToList();
}
}
public static unsafe Line TokenizeLine(string inputString)
{
if (string.IsNullOrEmpty(inputString) || !EnsureLibLoaded())
{
return new Line(inputString);
}
fixed (char* ptrInputStr = inputString)
{
var strRef = new slice_ref_uint16_t
{
len = (UIntPtr)inputString.Length,
ptr = (ushort*)ptrInputStr,
};
var tokens = Ffi.tokenize_line(strRef);
return tokens.ToLine(inputString);
}
}
private static string ExtractNativeLibrary(string libName)
{
string destinationPath = Path.Combine(Path.GetTempPath(), libName);

View File

@@ -1,11 +1,9 @@
namespace Slang;
using System;
using Assets.Scripts;
using Assets.Scripts.Objects;
using Assets.Scripts.Objects.Electrical;
using Assets.Scripts.Objects.Motherboards;
using Assets.Scripts.UI;
using HarmonyLib;
[HarmonyPatch]

View File

@@ -1,7 +1,3 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using HarmonyLib;
@@ -65,28 +61,6 @@ namespace Slang
}
}
/// <summary>
/// Encodes the original slang source code as base64 and uses gzip to compress it, returning the resulting string.
/// </summary>
public static string EncodeSource(string source)
{
if (string.IsNullOrEmpty(source))
{
return "";
}
byte[] bytes = Encoding.UTF8.GetBytes(source);
using (var memoryStream = new MemoryStream())
{
using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress))
{
gzipStream.Write(bytes, 0, bytes.Length);
}
return Convert.ToBase64String(memoryStream.ToArray());
}
}
public static bool IsSlangSource(ref string input)
{
return SlangSourceCheck.IsMatch(input);

View File

@@ -87,6 +87,21 @@ pub enum TokenType {
EOF,
}
impl From<TokenType> for u32 {
fn from(value: TokenType) -> Self {
use TokenType::*;
match value {
String(_) => 1,
Number(_) => 2,
Boolean(_) => 3,
Keyword(_) => 4,
Identifier(_) => 5,
Symbol(_) => 6,
EOF => 0,
}
}
}
impl std::fmt::Display for TokenType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {

View File

@@ -2,7 +2,10 @@ use compiler::Compiler;
use parser::Parser;
use safer_ffi::prelude::*;
use std::io::BufWriter;
use tokenizer::Tokenizer;
use tokenizer::{
token::{Token, TokenType},
Tokenizer,
};
#[derive_ReprC]
#[repr(C)]
@@ -98,6 +101,56 @@ pub fn compile_from_string(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::
}
#[ffi_export]
pub fn diagnose_source() -> safer_ffi::Vec<FfiDiagnostic> {
vec![].into()
pub fn tokenize_line(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<FfiToken> {
let tokenizer = Tokenizer::from(String::from_utf16_lossy(input.as_slice()));
let mut tokens = Vec::new();
// Error reporting is handled in `diagnose_source`. We only care about successful tokens here
// for syntax highlighting
for token in tokenizer {
if matches!(
token,
Ok(Token {
token_type: TokenType::EOF,
..
})
) {
continue;
}
match token {
Err(_) => {}
Ok(Token {
column,
original_string,
token_type,
..
}) => tokens.push(FfiToken {
column: column as i32,
error: "".into(),
length: (original_string.unwrap_or_default().len()) as i32,
token_kind: token_type.into(),
tooltip: "".into(),
}),
}
}
tokens.into()
}
#[ffi_export]
pub fn diagnose_source(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<FfiDiagnostic> {
let mut writer = BufWriter::new(Vec::new());
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 mut result_vec: Vec<FfiDiagnostic> = Vec::with_capacity(diagnosis.len());
for err in diagnosis {
result_vec.push(lsp_types::Diagnostic::from(err).into());
}
result_vec.into()
}