From fc13c465c0d20beff5a96e92d0f9c66579ffa545 Mon Sep 17 00:00:00 2001 From: Devin Bidwell Date: Tue, 30 Dec 2025 11:21:44 -0700 Subject: [PATCH] Extract logic into reusable functions for better DRY --- .github/copilot-instructions.md | 230 ++++++++++++++++++++++++++ rust_compiler/libs/compiler/src/v1.rs | 51 +++--- 2 files changed, 257 insertions(+), 24 deletions(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..cb9d715 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,230 @@ +# Slang Language Compiler - AI Agent Instructions + +## Project Overview + +**Slang** is a high-level programming language that compiles to IC10 assembly for the game Stationeers. The compiler is a multi-stage Rust system with a C# BepInEx mod integration layer. + +**Key Goal:** Reduce manual IC10 assembly writing by providing C-like syntax with automatic register allocation and device abstraction. + +## Architecture Overview + +### Compilation Pipeline + +The compiler follows a strict 4-stage pipeline (in [rust_compiler/libs/compiler/src/v1.rs](rust_compiler/libs/compiler/src/v1.rs)): + +1. **Tokenizer** (libs/tokenizer/src/lib.rs) - Lexical analysis using `logos` crate + + - Converts source text into tokens + - Tracks line/span information for error reporting + - Supports temperature literals (c/f/k suffixes) + +2. **Parser** (libs/parser/src/lib.rs) - AST construction + + - Recursive descent parser producing `Expression` tree + - Validates syntax, handles device declarations, function definitions + - Output: `Expression` enum containing tree nodes + +3. **Compiler (v1)** (libs/compiler/src/v1.rs) - Semantic analysis & code generation + + - Variable scope management and register allocation via `VariableManager` + - Emits IL instructions to `il::Instructions` + - Error types use `lsp_types::Diagnostic` for editor integration + +4. **Optimizer** (libs/optimizer/src/lib.rs) - Post-generation optimization + - Currently optimizes leaf functions + - Optional pass before final output + +### Cross-Language Integration + +- **Rust Library** (`slang.dll`/`.so`): Core compiler logic via `safer-ffi` C FFI bindings +- **C# Mod** (`StationeersSlang.dll`): BepInEx plugin integrating with game UI +- **Generated Headers** (via `generate-headers` binary): Auto-generated C# bindings from Rust + +### Key Types & Data Flow + +- `Expression` tree (parser) → `v1::Compiler` processes → `il::Instructions` output +- `InstructionNode` wraps IC10 assembly with optional source span for debugging +- `VariableManager` tracks scopes, tracks const/device/let distinctions +- `Operand` enum represents register/literal/device-property values + +## Critical Workflows + +### Building + +```bash +cd rust_compiler +# Build for both Linux and Windows targets +cargo build --release --target=x86_64-unknown-linux-gnu +cargo build --release --target=x86_64-pc-windows-gnu + +# Generate C# FFI headers (requires "headers" feature) +cargo run --features headers --bin generate-headers + +# Full build (run from root) +./build.sh +``` + +### Testing + +```bash +cd rust_compiler +# Run all tests +cargo test --package compiler --lib + +# Run specific test file +cargo test --package compiler --lib tuple_literals + +# Run single test +cargo test --package compiler --lib -- test::tuple_literals::test::test_tuple_literal_size_mismatch --exact --nocapture +``` + +### Quick Compilation + +```bash +cd rust_compiler +# Compile Slang code to IC10 using current compiler changes +echo 'let x = 5;' | cargo run --bin slang - +# Or from file +cargo run --bin slang -- input.slang -o output.ic10 +# Optimize the output with -z flag +cargo run --bin slang -- input.slang -o output.ic10 -z +``` + +## Codebase Patterns + +### Test Structure + +Tests follow a macro pattern in [libs/compiler/src/test/mod.rs](rust_compiler/libs/compiler/src/test/mod.rs): + +```rust +#[test] +fn test_name() -> Result<()> { + let output = compile!("slang code here"); + assert_eq!(expected_ic10, output); + Ok(()) +} +``` + +- `compile!()` macro: full pipeline from source to IC10 +- `compile!(result ...)` for error checking +- `compile!(debug ...)` for intermediate IR inspection +- Test files organize by feature: `binary_expression.rs`, `syscall.rs`, `tuple_literals.rs`, etc. + +### Error Handling + +All stages return custom Error types implementing `From`: + +- `tokenizer::Error` - Lexical errors +- `parser::Error<'a>` - Syntax errors +- `compiler::Error<'a>` - Semantic errors (unknown identifier, type mismatch) +- Device assignment prevention: `DeviceAssignment` error if reassigning device const + +### Variable Scope Management + +[variable_manager.rs](rust_compiler/libs/compiler/src/variable_manager.rs) handles: + +- Tracking const vs mutable (let) distinction +- Device declarations as special scope items +- Function-local scopes with parameter handling +- Register allocation via `VariableLocation` + +### LSP Integration + +Error types implement conversion to `lsp_types::Diagnostic` for IDE feedback: + +```rust +impl<'a> From> for lsp_types::Diagnostic { ... } +``` + +This enables real-time error reporting in the Stationeers IC10 Editor mod. + +## Project-Specific Conventions + +### Tuple Destructuring + +The compiler supports tuple returns and multi-assignment: + +```rust +let (x, y) = func(); // TupleDeclarationExpression +(x, y) = another_func(); // TupleAssignmentExpression +``` + +Compiler validates size matching with `TupleSizeMismatch` error. + +### Device Property Access + +Devices are first-class with property access: + +```rust +device ac = "d0"; +ac.On = true; +ac.Temperature > 20c; +``` + +Parsed as `MemberAccessExpression`, compiled to device I/O syscalls. + +### Temperature Literals + +Unique language feature - automatic unit conversion at compile time: + +```rust +20c → 293.15k // Celsius to Kelvin +68f → 293.15k // Fahrenheit to Kelvin +``` + +Tokenizer produces `Literal::Number(Number(decimal, Some(Unit::Celsius)))`. + +### Constants are Immutable + +Once declared with `const`, reassignment is a compile error. Device assignment prevention is critical (prevents game logic bugs). + +## Integration Points + +### C# FFI (`csharp_mod/FfiGlue.cs`) + +- Calls Rust compiler via marshaled FFI +- Passes source code, receives IC10 output +- Marshals errors as `Diagnostic` objects + +### BepInEx Plugin Lifecycle + +[csharp_mod/Plugin.cs](csharp_mod/Plugin.cs): + +- Harmony patches for IC10 Editor integration +- Cleanup code for live-reload support (mod destruction) +- Logger integration for debug output + +### CI/Build Target Matrix + +- Linux: `x86_64-unknown-linux-gnu` +- Windows: `x86_64-pc-windows-gnu` (cross-compile from Linux) +- Both produce dynamic libraries + CLI binary + +## Debugging Tips + +1. **Print source spans:** `Span` type tracks line/column for error reporting +2. **IL inspection:** Use `compile!(debug source)` to view intermediate instructions +3. **Register allocation:** `VariableManager` logs scope changes; check for conflicts +4. **Syscall validation:** [parser/src/sys_call.rs](rust_compiler/libs/parser/src/sys_call.rs) lists all valid syscalls +5. **Tokenizer issues:** Check [tokenizer/src/token.rs](rust_compiler/libs/tokenizer/src/token.rs) for supported keywords/symbols + +## Key Files for Common Tasks + +| Task | File | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| Add language feature | [libs/parser/src/lib.rs](rust_compiler/libs/parser/src/lib.rs) + test in [libs/compiler/src/test/](rust_compiler/libs/compiler/src/test/) | +| Fix codegen bug | [libs/compiler/src/v1.rs](rust_compiler/libs/compiler/src/v1.rs) (~3500 lines) | +| Add syscall | [libs/parser/src/sys_call.rs](rust_compiler/libs/parser/src/sys_call.rs) | +| Optimize output | [libs/optimizer/src/lib.rs](rust_compiler/libs/optimizer/src/lib.rs) | +| Mod integration | [csharp_mod/](csharp_mod/) | +| Language docs | [docs/language-reference.md](docs/language-reference.md) | + +## Dependencies to Know + +- `logos` - Tokenizer with derive macros +- `rust_decimal` - Precise decimal arithmetic for temperature conversion +- `safer-ffi` - Safe C FFI between Rust and C# +- `lsp-types` - Standard for editor diagnostics +- `thiserror` - Error type derivation +- `clap` - CLI argument parsing +- `anyhow` - Error handling in main binary diff --git a/rust_compiler/libs/compiler/src/v1.rs b/rust_compiler/libs/compiler/src/v1.rs index 40897e9..510fa2e 100644 --- a/rust_compiler/libs/compiler/src/v1.rs +++ b/rust_compiler/libs/compiler/src/v1.rs @@ -301,6 +301,29 @@ impl<'a> Compiler<'a> { Cow::from(format!("__internal_L{}", self.label_counter)) } + /// Merges two spans into a single span covering both + fn merge_spans(start: Span, end: Span) -> Span { + Span { + start_line: start.start_line, + start_col: start.start_col, + end_line: end.end_line, + end_col: end.end_col, + } + } + + /// Cleans up temporary variables, ignoring errors + fn cleanup_temps( + scope: &mut VariableScope<'a, '_>, + temps: &[Option>], + ) -> Result<(), Error<'a>> { + for temp in temps { + if let Some(name) = temp { + scope.free_temp(name.clone(), None)?; + } + } + Ok(()) + } + fn expression( &mut self, expr: Spanned>, @@ -2094,12 +2117,7 @@ impl<'a> Compiler<'a> { } }; - let span = Span { - start_line: left_expr.span.start_line, - start_col: left_expr.span.start_col, - end_line: right_expr.span.end_line, - end_col: right_expr.span.end_col, - }; + let span = Self::merge_spans(left_expr.span, right_expr.span); // Compile LHS let (lhs, lhs_cleanup) = self.compile_operand(*left_expr, scope)?; @@ -2118,12 +2136,7 @@ impl<'a> Compiler<'a> { )?; // Clean up operand temps - if let Some(name) = lhs_cleanup { - scope.free_temp(name, None)?; - } - if let Some(name) = rhs_cleanup { - scope.free_temp(name, None)?; - } + Self::cleanup_temps(scope, &[lhs_cleanup, rhs_cleanup])?; Ok(CompileLocation { location: result_loc, @@ -2199,12 +2212,7 @@ impl<'a> Compiler<'a> { LogicalExpression::Not(_) => unreachable!(), }; - let span = Span { - start_line: left_expr.span.start_line, - start_col: left_expr.span.start_col, - end_line: right_expr.span.end_line, - end_col: right_expr.span.end_col, - }; + let span = Self::merge_spans(left_expr.span, right_expr.span); // Compile LHS let (lhs, lhs_cleanup) = self.compile_operand(*left_expr, scope)?; @@ -2224,12 +2232,7 @@ impl<'a> Compiler<'a> { )?; // Clean up operand temps - if let Some(name) = lhs_cleanup { - scope.free_temp(name, None)?; - } - if let Some(name) = rhs_cleanup { - scope.free_temp(name, None)?; - } + Self::cleanup_temps(scope, &[lhs_cleanup, rhs_cleanup])?; Ok(CompileLocation { location: result_loc,