97 Commits

Author SHA1 Message Date
f87fdc1b0a Added another test to ensure all 3 tuple scenerios are covered 2025-12-30 20:09:45 -07:00
72e6981176 Update tests to reflect new changes with stack cleanup in functions that return tuples 2025-12-30 20:05:10 -07:00
d83341d90b Update tuples to support member access and function calls 2025-12-30 16:33:11 -07:00
d297f1bd46 Update changelog 2025-12-30 12:52:51 -07:00
90a2becbbb Bump version to 5.0 2025-12-30 12:51:24 -07:00
a53ea7fd13 removed debug variant macro 2025-12-30 12:49:28 -07:00
c1a8af6aa7 Refactored remaining tests to use check macro 2025-12-30 12:34:47 -07:00
8c8ae23a27 wip -- convert remaining tests to use check 2025-12-30 12:28:53 -07:00
04c205b31d Fixed compiler bug as a result of the 'check' test variant 2025-12-30 12:05:54 -07:00
c133dc3c80 Refactor tests to use new check variant 2025-12-30 11:58:31 -07:00
9d8a867e5f Add new macro variant 'check' to ensure there are no errors AND the compiled output matches 2025-12-30 11:53:02 -07:00
e2a45f0d05 Added more tests and updated existing to use snapshot style testing 2025-12-30 11:49:42 -07:00
fc13c465c0 Extract logic into reusable functions for better DRY 2025-12-30 11:21:44 -07:00
1ce3162fc0 Refactor Compiler struct to hold FunctionMetadata struct instead of flattening all that information directly onto the Compiler 2025-12-30 11:15:49 -07:00
3092e97d41 Minor DRY refactor. Added more tuple tests 2025-12-30 02:47:39 -07:00
8029fa82b0 complex tuple expressions supported 2025-12-30 02:38:32 -07:00
6d8a22459c wip 2025-12-30 02:31:21 -07:00
20f0f4b9a1 working tuple types 2025-12-30 00:58:02 -07:00
5a88befac9 tuple return types just about implemented 2025-12-30 00:32:55 -07:00
e94fc0f5de Functions returning tuples somewhat working, but they clobber the popped ra 2025-12-29 23:55:00 -07:00
b51800eb77 wip -- tuples compiling. need more work on function invocations 2025-12-29 23:17:18 -07:00
87951ab12f Support tuple assignment expressions and tuple assignments and declarations with function invocations 2025-12-29 22:33:16 -07:00
00b0d4df26 Create new tuple expression types 2025-12-29 22:17:19 -07:00
6ca53e8959 Merge pull request 'Added support for CRLF windows line endings' (#11) from 41-support-windows-crlf-line-endings into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m42s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #11
2025-12-29 12:32:59 -07:00
8dfdad3f34 Added support for CRLF windows line endings
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-29 12:29:01 -07:00
e272737ea2 Merge pull request 'Fixed const -> let bug' (#10) from declaration_const_as_let into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 32s
CI/CD Pipeline / build (push) Successful in 1m43s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #10
2025-12-29 02:44:50 -07:00
f679601818 Fixed const -> let bug
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 33s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-29 02:31:23 -07:00
3ca6f97db1 Merge pull request 'IC10Editor fix' (#9) from live-reload into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m41s
CI/CD Pipeline / release (push) Successful in 4s
Reviewed-on: #9
2025-12-27 22:26:50 -07:00
34817ee111 Merge branch 'master' of ssh://git.biddydev.com:2222/dbidwell/stationeers_lang into live-reload
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 32s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-27 22:19:09 -07:00
9eef8a77b6 Merge pull request 'Update About.xml docs for the workshop' (#8) from documentation into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 34s
CI/CD Pipeline / build (push) Successful in 1m41s
CI/CD Pipeline / release (push) Has been skipped
Reviewed-on: #8
2025-12-27 22:19:00 -07:00
de31851153 Fixed IC10Editor implementation bug where IC10 code would not update after clicking 'Cancel' 2025-12-27 22:18:21 -07:00
3543b87561 wip 2025-12-27 16:03:36 -07:00
effef64add Update About.xml docs for the workshop
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 33s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-26 22:33:48 -07:00
794b27b8c6 Merge pull request 'documentation' (#7) from documentation into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m42s
CI/CD Pipeline / release (push) Has been skipped
Reviewed-on: #7
2025-12-26 22:02:46 -07:00
27e8987831 removed the temporary example slang scripts from the root directory
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-26 22:00:28 -07:00
0fdceac22c changed markdown tags from slang to rust 2025-12-26 21:57:11 -07:00
85f8b136e1 Update markdown tags to use rust instead of mips 2025-12-26 21:56:21 -07:00
c91086157a Added documentation for various language features and in-game functions. Added example scripts 2025-12-26 21:55:36 -07:00
6bee591484 Merge pull request 'Added stationpedia docs back into the game patches' (#6) from docs-fix into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m44s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #6
2025-12-26 16:20:16 -07:00
c3c14cec23 Added stationpedia docs back into the game patches
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 33s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-26 16:18:24 -07:00
4e885847a8 Merge pull request 'Remove MOD from Plugin.cs which fixed networking' (#5) from slp-removal into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 34s
CI/CD Pipeline / build (push) Successful in 1m43s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #5
2025-12-24 22:28:51 -07:00
0ca6b27a11 Remove MOD from Plugin.cs which fixed networking
All checks were successful
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
CI/CD Pipeline / test (pull_request) Successful in 34s
2025-12-24 22:27:43 -07:00
9b8900d7a7 Merge pull request 'ic10editor-update' (#4) from ic10editor-update into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m43s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #4
2025-12-24 12:41:00 -07:00
792bba4875 Removed unused macro imports as they are implicit
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 33s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 12:39:21 -07:00
1c39e146fb Clear editor selection for IC10 if no slang source maps to an IC10 source
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 12:36:33 -07:00
47bcd0be34 Cleaned up BepInEx logging
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 12:10:11 -07:00
445f731170 Fixed IC10 ouput window, refactored IC10 highlighting
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 33s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 12:08:17 -07:00
c7aa30581d Added logging around the creation of the IC10Editor tab
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 11:10:10 -07:00
42b0b0acf9 Working saving / loading from the IC10Editor mod. Removed all patches until they can be properly re-implemented
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 35s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-24 01:24:16 -07:00
5230c620e8 WIP -- before another refactor 2025-12-23 20:32:27 -07:00
06a0ec28eb Modify IC10 view logic to conform to the new IC10Editor update 2025-12-22 17:45:42 -07:00
73e08b9896 Merge pull request 'Fix for Gitea actions' (#3) from actions-fix into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m45s
CI/CD Pipeline / release (push) Successful in 15s
Reviewed-on: #3
2025-12-21 16:42:59 -07:00
e83ff67af8 Fix for Gitea actions
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-21 16:42:29 -07:00
cacff4ff55 Merge pull request '35-accept-variables' (#2) from 35-accept-variables into master
Some checks failed
CI/CD Pipeline / test (push) Successful in 34s
CI/CD Pipeline / build (push) Failing after 1m55s
CI/CD Pipeline / release (push) Has been skipped
Reviewed-on: #2
2025-12-21 16:34:26 -07:00
7295b14f6a Update changelog, update workflow files
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-21 16:32:11 -07:00
93873dfa93 Accept expressions for the slotIndex in the slot logic syscalls 2025-12-21 15:59:40 -07:00
15752fde3d Merge pull request #34 from dbidwell94/inline-ic10
Inline ic10
2025-12-20 19:07:45 -07:00
badcdd3c31 Removed unneeded array sort 2025-12-20 17:47:35 -07:00
f0e7506905 Remove dead code and change some comments 2025-12-20 17:35:22 -07:00
0962b3a5e7 highlight background of IC10 for the current caret position of the Slang script 2025-12-20 17:32:20 -07:00
1439f9ee7e Remove conditional IC10 formatting 2025-12-19 21:58:47 -07:00
3f105ef35c update changelog and version bump 2025-12-19 21:08:19 -07:00
45a7a6b38b wip -- show ic10 alongside of Slang 2025-12-19 20:46:24 -07:00
5dbb0ee2d7 Merge pull request #33 from dbidwell94/reagent
[0.3.4]

- Added support for `loadReagent`, which maps to the `lr` IC10 instruction
  - Shorthand is `lr`
  - Longform is `loadReagent`
- Update various Rust dependencies
- Added more optimizations, prioritizing `pop` instead of `get` when available
  when backing up / restoring registers for function invocations. This should
  save approximately 2 lines per backed up register
2025-12-17 21:18:16 -07:00
6b18489f54 Added more optimizations in regards to function invocations and backing
up and restoring registers
2025-12-17 21:05:01 -07:00
ecfed65221 Update rust dependencies 2025-12-17 18:02:34 -07:00
ed5ea9f6eb update changelog and version bump 2025-12-17 17:57:37 -07:00
0b354d4ec0 First pass getting loadReagent support into the compiler with optimizations 2025-12-17 17:49:34 -07:00
6c11c0e6e5 Merge pull request #31 from dbidwell94/30-temp-literal-negatives
0.3.3
2025-12-15 23:19:14 -07:00
88b6571659 update changelog and version bump 2025-12-15 23:15:41 -07:00
477c2b1aef Fixed bug where temperature literals were not being calculated correctly with negative numbers 2025-12-15 23:13:40 -07:00
941e81a3e5 Merge pull request #29 from dbidwell94/overflow-bug
Overflow bug
2025-12-14 03:27:12 -07:00
b98817c8a0 Fixed tests to show new line label convention for internal labels 2025-12-14 03:23:49 -07:00
6d5c179eac Fixed stack overflow due to improper handling of leaf functions 2025-12-14 03:16:58 -07:00
b7fbc499b6 WIP fix stack overflow 2025-12-14 02:54:56 -07:00
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
57 changed files with 10105 additions and 1598 deletions

View File

@@ -4,6 +4,7 @@ name: CI/CD Pipeline
on:
push:
branches: ["master"]
tags: ["*.*.*"]
pull_request:
branches: ["master"]
@@ -57,6 +58,10 @@ jobs:
slang-builder \
./build.sh
- name: Zip Workshop Folder
run: |
zip -r release/workshop.zip release/workshop/
# 3. Fix Permissions
# Docker writes files as root. We need to own them to upload them.
- name: Fix Permissions
@@ -65,7 +70,36 @@ jobs:
# 4. Upload to GitHub
- name: Upload Release Artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: StationeersSlang-Release
path: release/
release:
needs: build
runs-on: self-hosted
# ONLY run this job if we pushed a tag (e.g., v1.0.1)
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4
# We download the artifact from the previous 'build' job
- name: Download Build Artifacts
uses: actions/download-artifact@v3
with:
name: StationeersSlang-Release
path: ./release-files
- name: Create Gitea Release
uses: https://gitea.com/actions/gitea-release-action@v1
with:
files: |
./release-files/workshop.zip
./release-files/slang
./release-files/slang.exe
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

235
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,235 @@
# 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!(debug "slang code here");
assert_eq!(
output,
indoc! {
"Expected IC10 output here"
}
);
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<lsp_types::Diagnostic>`:
- `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<Error<'a>> 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

View File

@@ -1,5 +1,121 @@
# Changelog
[0.5.0]
- Added support for tuple types
- Added support for tuple returns from functions
- Added support for ignoring tuple values
- Fixed various compiler bugs
[0.4.7]
- Added support for Windows CRLF endings
[0.4.6]
- Fixed bug in compiler where you were unable to assign a `const` value to
a `let` variable
[0.4.5]
- Fixed issue where after clicking "Cancel" on the IC10 Editor, the side-by-side
IC10 output would no longer update with highlighting or code updates.
- Added ability to live-reload the mod while developing using the `ScriptEngine`
mod from BepInEx
- This required adding in cleanup code to cleanup references to the Rust DLL
before destroying the mod instance.
- Added BepInEx debug logging. This will ONLY show if you have debug logs
enabled in the BepInEx configuration file.
[0.4.4]
- Added Stationpedia docs back after removing all harmony patches from the mod
[0.4.3]
- Removed references to the `Mod` class from SLP. This was the root of the multiplayer
connectivity issues. Multiplayer should now work with Slang installed.
[0.4.2]
- Removed all harmony patches as most functionality as been added into the
`IC10 Editor` mod
- IC10 runtime errors will have been reverted back to showing as IC10 line
numbers instead of Slang line numbers.
- The IC10 line should be easily mapped to a Slang line via the side-by-side
IC10 compilation view.
[0.4.1]
- Update syscalls for `loadSlot` and `setSlot` to support expressions instead of
just variables for the slot index
- Moved the main repository from GitHub to a self-hosted Gitea
- Restructured workflow files to support this change
- GitHub will still remain as a mirrored repository of the new
Gitea instance.
- This is in response to the new upcoming changes to the pricing model
for self-hosted GitHub action runners.
[0.4.0]
- First pass getting compiled IC10 to output along side the Slang source code
- IC10 side is currently not scrollable, and text might be cut off from the bottom,
requiring newlines to be added to the bottom of the Slang source if needed
[0.3.4]
- Added support for `loadReagent`, which maps to the `lr` IC10 instruction
- Shorthand is `lr`
- Longform is `loadReagent`
- Update various Rust dependencies
- Added more optimizations, prioritizing `pop` instead of `get` when available
when backing up / restoring registers for function invocations. This should
save approximately 2 lines per backed up register
[0.3.3]
- Fixed bug where negative temperature literals were converted to Kelvin
first before applying the negative
[0.3.2]
- Fixed stack overflow due to incorrect optimization of 'leaf' functions
[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`

View File

@@ -2,11 +2,11 @@
<ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Slang</Name>
<Author>JoeDiertay</Author>
<Version>0.2.1</Version>
<Version>0.5.0</Version>
<Description>
[h1]Slang: High-Level Programming for Stationeers[/h1]
Stop writing assembly. Start writing code.
Iterate faster. 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 IC10 instantly.
@@ -15,7 +15,7 @@ Slang (Stationeers Language) brings modern programming to Stationeers. It allows
[h2]Features[/h2]
[list]
[*] [b]In-Game Compilation:[/b] No external tools needed. Write Slang directly in the IC editor.
[*] [b]No More Register Juggling:[/b] Define variables with let (e.g., let temp = 300). The compiler manages r0-r15 for you.
[*] [b]No More Register Juggling:[/b] Define variables with let (e.g., let temp = 200c). The compiler manages r0-r15 for you.
[*] [b]Control Flow:[/b] Write readable logic with if, else, while, loop, break, and continue.
[*] [b]Functions:[/b] Create reusable code blocks with arguments.
[*] [b]Smart Editor:[/b] Get real-time syntax highlighting and error checking (red text) as you type.
@@ -23,6 +23,8 @@ Slang (Stationeers Language) brings modern programming to Stationeers. It allows
[*] [b]Optimizations:[/b] Features like Constant Folding calculate math at compile time to save instructions.
[*] [b]Device Aliasing:[/b] Simple mapping: device sensor = "d0".
[*] [b]Temperature Literals:[/b] Don't worry about converting Celsius to Kelvin anymore. Define your temperatures as whatever you want and append the proper suffix at the end (ex. 20c, 68f, 293.15k)
[*] [b]Side-by-side IC10 output:[/b] Preview the compiled IC10 alongside the Slang source code. What you see is what you get.
[*] [b]Compiler Optimizations:[/b] Slang now does its best to safely optimize the output IC10, removing labels, unnecessary moves, etc.
[/list]
[h2]Installation[/h2]
@@ -50,19 +52,14 @@ loop {
[h2]Known Issues (Beta)[/h2]
[list]
[*] [b]Code Size:[/b] Compiled output is currently more verbose than hand-optimized assembly. Optimization passes are planned.
[*] [b]Stack Access:[/b] Direct stack memory access is disabled to prevent conflicts with the compiler's internal memory management.
[*] [b]Documentation:[/b] In-game tooltips for syscalls (like load, set) are WIP. Check the "Slang" entry in the Stationpedia (F1) for help.
[*] [b]Debugging:[/b] Runtime errors currently point to the compiled IC10 line number, not your Slang source line. Source mapping is coming soon.
[*] [b]Stack Access:[/b] Direct stack memory access is disabled to prevent conflicts with the compiler's internal memory management. A workaround is being planned.
[*] [b]Documentation:[/b] In-game tooltips for syscalls (like load, set) are WIP. Check the "Slang" entry in the Stationpedia (F1) for help, or checkout [url=https://github.com/dbidwell94/stationeers_lang/blob/master/docs/getting-started.md]The Docs[/url] for guides on how to get started.
[/list]
[h2]Planned Features[/h2]
[list]
[*] Side-by-side view: Slang vs. Compiled IC10.
[*] Compiler optimizations (dead code elimination, smarter register allocation).
[*] Enhanced LSP features (Autocomplete, Go to Definition).
[*] Full feature parity with all IC10 instructions.
[*] Tutorials and beginner script examples.
[/list]
[h2]FAQ[/h2]
@@ -72,10 +69,18 @@ A: The Slang compiler is built in Rust for performance and reliability. It is co
[b]Q: Is this compatible with my current save?[/b]
A: Yes! Slang does not modify any existing IC10 code, it is only a compiler. As a matter of fact: if you wish to stop using Slang at any time, your compiled IC10 will still exist on the chip. However: Slang adds a comment at the bottom of your compiled IC10 which is a GZIP and base64 encoded version of your Slang source code. This might break line limits with Slang not installed. If you wish to no longer use Slang, I recommend you remove this comment from the source code after uninstalling Slang.
[b]Q: Does this modify the in-game scripting language[/b]
A: No! Slang compiles directly to IC10. Any valid Slang file will produce valid IC10. The goal of this mod is twofold:
[list]
[*] Allow experienced users to quickly iterate on their scripts to get back into base upgrades faster
[*] Allow newcomers who already know C-style languages (JS/C#/Java/Rust/C/etc) to see how it maps to IC10 so they can get to understand IC10 better
[/list]
[h2]Useful Links[/h2]
[url=https://github.com/dbidwell94/stationeers_lang]Source Code on GitHub[/url]
[url=https://discord.gg/stationeers]Stationeers Official Discord[/url]
[url=https://discord.gg/M4sCfYMacs]Stationeers Modding Discord[/url]
[url=https://github.com/dbidwell94/stationeers_lang/blob/master/docs/getting-started.md]Getting Started Guide[/url]
</Description>
<ChangeLog xsi:nil="true" />
<WorkshopHandle>3619985558</WorkshopHandle>
@@ -88,7 +93,7 @@ A: Yes! Slang does not modify any existing IC10 code, it is only a compiler. As
<Tag>Quality of Life</Tag>
</Tags>
<DependsOn WorkshopHandle="3592775931" />
<OrderBefore WorkshopHandle="3592775931" />
<OrderAfter WorkshopHandle="3592775931" />
<InGameDescription><![CDATA[
<size=30><color=#ffff00>Slang - High Level Language Compiler</color></size>
A modern programming experience for Stationeers. Write C-style code that compiles to IC10 instantly.
@@ -118,5 +123,7 @@ A: Yes! Slang does not modify any existing IC10 code, it is only a compiler. As
See: https://github.com/StationeersLaunchPad/StationeersLaunchPad
Source Code: https://github.com/dbidwell94/stationeers_lang
Documentation: https://github.com/dbidwell94/stationeers_lang/blob/master/docs/getting-started.md
]]></InGameDescription>
</ModMetadata>

View File

@@ -1,30 +1,45 @@
# Stationeers Language (slang)
This is an ambitious attempt at creating:
- A new programming language (slang)
- A compiler to translate slang -> IC10
- A mod to allow direct input of slang in the in-game script editor to
automatically compile to IC10 before running
This project currently outputs 3 files:
- A Linux CLI
- A Windows CLI
- A Windows FFI dll
- Contains a single function: `compile_from_string`
The aim of this project is to lower the amount of time it takes to code simple
scripts in Stationeers so you can get back to engineering atmospherics or
whatever you are working on. This project is NOT meant to fully replace IC10.
Obviously hand-coded assembly written by an experienced programmer is more
optimized and smaller than something that a C compiler will spit out. This is
the same way. It WILL produce valid IC10, but for large complicated projects it
might produce over the allowed limit of lines the in-game editor supports.
Current Unknowns
- Should I support a configurable script line length in-game to allow larger
scripts to be saved?
- Should compilation be "behind the scenes" (in game editor will ALWAYS be what
you put in. IC10 will be IC10, slang will be slang)
# Slang Language Documentation
Slang is a high-level programming language that compiles to IC10 assembly for [Stationeers](https://store.steampowered.com/app/544550/Stationeers/).
It provides a familiar C-like syntax while targeting the limited instruction set
of in-game IC10.
## Quick Links
- [Getting Started](docs/getting-started.md) - Installation and first program
- [Language Reference](docs/language-reference.md) - Complete syntax guide
- [Built-in Functions](docs/builtins.md) - System calls and math functions
- [Examples](docs/examples.md) - Real-world code samples
## Overview
Slang aims to reduce the time spent writing IC10 assembly by providing:
- **Familiar syntax** - C-like declarations, control flow, and expressions
- **Device abstraction** - Named device bindings with property access
- **Automatic register allocation** - No manual register management
- **Built-in functions** - Math operations and device I/O as function calls
- **Temperature literals** - Native support for Celsius, Fahrenheit, and Kelvin
## Example
```rust
device gasSensor = "d0";
device airCon = "d1";
const TARGET_TEMP = 20c;
loop {
yield();
airCon.On = gasSensor.Temperature > TARGET_TEMP;
}
```
This compiles to IC10 that monitors temperature and controls an air
conditioner.
## Project Status
Slang is under active development. It may produce suboptimal code for complex programs.
It is not a replacement for IC10, for performance-critical or large scripts,
hand-written IC10 may still be preferred.

View File

@@ -113,6 +113,34 @@ public static unsafe class SlangExtensions
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)
{
switch (kind)

View File

@@ -71,18 +71,6 @@ public unsafe struct Vec_uint8_t {
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)]
public unsafe struct FfiRange_t {
public UInt32 start_col;
@@ -94,6 +82,44 @@ public unsafe struct FfiRange_t {
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)]
public unsafe struct FfiDiagnostic_t {
public Vec_uint8_t message;
@@ -146,6 +172,12 @@ public unsafe partial class Ffi {
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 {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
void free_ffi_diagnostic_vec (

View File

@@ -9,10 +9,33 @@ using StationeersIC10Editor;
public class SlangFormatter : ICodeFormatter
{
public const string SLANG_SRC = "SLANG_SRC";
private CancellationTokenSource? _lspCancellationToken;
private object _tokenLock = new();
// VS Code Dark Theme Palette
protected Editor? __Ic10Editor = null;
protected Editor Ic10Editor
{
get
{
if (__Ic10Editor == null)
{
var tab = Editor.ParentTab;
tab.ClearExtraEditors();
__Ic10Editor = new Editor(Editor.KeyHandler);
Ic10Editor.IsReadOnly = true;
tab.AddEditor(__Ic10Editor);
}
return __Ic10Editor;
}
}
private string ic10CompilationResult = "";
private List<SourceMapEntry> ic10SourceMap = new();
#region Colors
public static readonly uint ColorControl = ColorFromHTML("#C586C0"); // Pink (if, return, loop)
public static readonly uint ColorDeclaration = ColorFromHTML("#569CD6"); // Blue (let, device, fn)
public static readonly uint ColorFunction = ColorFromHTML("#DCDCAA"); // Yellow (syscalls)
@@ -21,10 +44,8 @@ public class SlangFormatter : ICodeFormatter
public static readonly uint ColorBoolean = ColorFromHTML("#569CD6"); // Blue (true/false)
public static readonly uint ColorIdentifier = ColorFromHTML("#9CDCFE"); // Light Blue (variables)
public static new readonly uint ColorDefault = ColorFromHTML("#D4D4D4"); // White (punctuation ; { } )
// Operators are often the same color as default text in VS Code Dark,
// but having a separate definition lets you tweak it (e.g. make them slightly darker or distinct)
public static readonly uint ColorOperator = ColorFromHTML("#D4D4D4");
#endregion
private HashSet<uint> _linesWithErrors = new();
private int _lastLineCount = -1;
@@ -33,6 +54,7 @@ public class SlangFormatter : ICodeFormatter
: base()
{
OnCodeChanged += HandleCodeChanged;
OnCaretMoved += UpdateIc10Formatter;
}
public static double MatchingScore(string input)
@@ -41,6 +63,11 @@ public class SlangFormatter : ICodeFormatter
if (string.IsNullOrWhiteSpace(input))
return 0d;
if (input.Contains(SLANG_SRC))
{
return 1.0;
}
// Run the compiler to get diagnostics
var diagnostics = Marshal.DiagnoseSource(input);
@@ -67,7 +94,28 @@ public class SlangFormatter : ICodeFormatter
public override string Compile()
{
return this.Lines.RawText;
if (!Marshal.CompileFromString(RawText, out var compilationResult, out var sourceMap))
{
return "# Compilation Error";
}
return compilationResult + $"\n{EncodeSource(RawText, SLANG_SRC)}";
}
public override void ResetCode(string code)
{
// for compatibility, we need to check for GlobalCode.SLANG_SRC
// `#SLANG_SRC:<code>`
// and replace with `# SLANG_SRC: <code>`
if (code.Contains(GlobalCode.SLANG_SRC))
{
code = code.Replace(GlobalCode.SLANG_SRC, $"# {SLANG_SRC}: ");
}
if (code.Contains(SLANG_SRC))
{
code = ExtractEncodedSource(code, SLANG_SRC);
}
base.ResetCode(code);
}
public override StyledLine ParseLine(string line)
@@ -86,6 +134,10 @@ public class SlangFormatter : ICodeFormatter
return styledLine;
}
/// <summary>
/// This handles calling the `HandleLsp` function by creating a new `CancellationToken` and
/// cancelling the current call if applicable.
/// </summary>
private void HandleCodeChanged()
{
CancellationToken token;
@@ -101,6 +153,11 @@ public class SlangFormatter : ICodeFormatter
_ = HandleLsp(inputSrc, token);
}
/// <summary>
/// Takes a copy of the current source code and sends it to the Rust compiler in a background thread
/// to get diagnostic data. This also handles getting a compilation response of optimized IC10 for the
/// side-by-side IC10Editor to show with sourcemap highlighting.
/// </summary>
private async Task HandleLsp(string inputSrc, CancellationToken cancellationToken)
{
try
@@ -126,6 +183,29 @@ public class SlangFormatter : ICodeFormatter
);
ApplyDiagnostics(dict);
// If we have valid code, update the IC10 output
if (dict.Count > 0)
{
return;
}
var (compilationSuccess, compiled, sourceMap) = await Task.Run(() =>
{
var successful = Marshal.CompileFromString(
inputSrc,
out var compiled,
out var sourceMap
);
return (successful, compiled, sourceMap);
});
if (compilationSuccess)
{
ic10CompilationResult = compiled;
ic10SourceMap = sourceMap;
UpdateIc10Content(Ic10Editor);
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
@@ -134,7 +214,62 @@ public class SlangFormatter : ICodeFormatter
}
}
// This runs on the Main Thread
/// <summary>
/// Updates the underlying code in the IC10 Editor, after which will call `UpdateIc10Formatter` to
/// update highlighting of relavent fields.
/// </summary>
private void UpdateIc10Content(Editor editor)
{
editor.ResetCode(ic10CompilationResult);
UpdateIc10Formatter();
}
// This runs on the main thread. This function ONLY updates the highlighting of the IC10 code.
// If you need to update the code in the editor itself, you should use `UpdateIc10Content`.
private void UpdateIc10Formatter()
{
// Bail if our backing field is null. We don't want to set the field in this function. It
// runs way too much and we might not even have source code to use.
if (__Ic10Editor == null)
return;
var caretPos = Editor.CaretPos.Line;
// get the slang sourceMap at the current editor line
var lines = ic10SourceMap.FindAll(entry =>
entry.SlangSource.StartLine == caretPos || entry.SlangSource.EndLine == caretPos
);
Ic10Editor.ResetCode(ic10CompilationResult);
if (lines.Count() < 1)
{
Ic10Editor.Selection = new TextRange
{
End = new TextPosition { Col = 0, Line = 0 },
Start = new TextPosition { Col = 0, Line = 0 },
};
return;
}
// get the total range of the IC10 source for the selected Slang line
var max = lines.Max(line => line.Ic10Line);
var min = lines.Min(line => line.Ic10Line);
Ic10Editor.CaretPos = new TextPosition { Col = 0, Line = (int)max };
// highlight all the IC10 lines that are within the specified range
Ic10Editor.Selection.Start = new TextPosition { Col = 0, Line = (int)min };
Ic10Editor.Selection.End = new TextPosition
{
Col = Ic10Editor.Lines[(int)max].Text.Length,
Line = (int)max,
};
}
/// <summary>
/// Takes diagnostics from the Rust FFI compiler and applies it as semantic tokens to the
/// source in this editor.
/// This runs on the Main Thread
/// </summary>
private void ApplyDiagnostics(Dictionary<uint, IGrouping<uint, Diagnostic>> dict)
{
HashSet<uint> linesToRefresh;

View File

@@ -1,99 +1,69 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace Slang;
public static class GlobalCode
{
public const string SLANG_REF = "#SLANG_REF:";
/// <summary>
/// This is the OLD way of handling saving / loading. This has been replaced with a native
/// save / load from IC10 Editor. However; this needs to remain for compatibility with
/// previous versions of code not compiled with 0.4.2 or later.
/// </summary>
public const string SLANG_SRC = "#SLANG_SRC:";
// This is a Dictionary of ENCODED source code, compressed
// so that save file data is smaller
private static Dictionary<Guid, string> codeDict = new();
/// <summary>
/// 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
/// </summary>
private static Dictionary<Guid, Dictionary<uint, List<Range>>> sourceMaps = new();
public static void ClearCache()
public static void SetSourceMap(Guid reference, List<SourceMapEntry> sourceMapEntries)
{
codeDict.Clear();
}
var builtDictionary = new Dictionary<uint, List<Range>>();
public static string GetSource(Guid reference)
{
if (!codeDict.ContainsKey(reference))
foreach (var entry in sourceMapEntries)
{
return string.Empty;
}
return DecodeSource(codeDict[reference]);
}
public static void SetSource(Guid reference, string source)
{
codeDict[reference] = EncodeSource(source);
}
public static string? GetEncoded(Guid reference)
{
if (!codeDict.ContainsKey(reference))
return null;
return codeDict[reference];
}
public static void SetEncoded(Guid reference, string encodedSource)
{
if (codeDict.ContainsKey(reference))
{
codeDict[reference] = encodedSource;
}
else
{
codeDict.Add(reference, encodedSource);
}
}
private 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))
if (!builtDictionary.ContainsKey(entry.Ic10Line))
{
gzipStream.Write(bytes, 0, bytes.Length);
builtDictionary[entry.Ic10Line] = new();
}
return Convert.ToBase64String(memoryStream.ToArray());
builtDictionary[entry.Ic10Line].Add(entry.SlangSource);
}
sourceMaps[reference] = builtDictionary;
}
private static string DecodeSource(string source)
public static bool GetSlangErrorLineFromICError(
Guid reference,
uint icErrorLine,
out uint slangSrc,
out Range slangSpan
)
{
if (string.IsNullOrEmpty(source))
slangSrc = icErrorLine;
slangSpan = new Range { };
if (!sourceMaps.ContainsKey(reference))
{
return "";
return false;
}
byte[] compressedBytes = Convert.FromBase64String(source);
using (var memoryStream = new MemoryStream(compressedBytes))
if (!sourceMaps[reference].ContainsKey(icErrorLine))
{
using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
{
using (var outputStream = new MemoryStream())
{
gzipStream.CopyTo(outputStream);
return Encoding.UTF8.GetString(outputStream.ToArray());
}
}
return false;
}
var foundRange = sourceMaps[reference][icErrorLine];
if (foundRange is null)
{
return false;
}
slangSrc = foundRange[0].StartLine;
slangSpan = foundRange[0];
return true;
}
}

View File

@@ -10,10 +10,23 @@ using StationeersIC10Editor;
public struct Range
{
public uint StartCol;
public uint EndCol;
public uint StartLine;
public uint EndLine;
public uint StartCol = 0;
public uint EndCol = 0;
public uint StartLine = 0;
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
@@ -23,6 +36,17 @@ public struct Diagnostic
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
{
private static IntPtr _libraryHandle = IntPtr.Zero;
@@ -43,7 +67,9 @@ public static class Marshal
try
{
_libraryHandle = LoadLibrary(ExtractNativeLibrary(Ffi.RustLib));
L.Debug("Rust DLL loaded successfully. Enjoy native speed compilations!");
CodeFormatters.RegisterFormatter("Slang", typeof(SlangFormatter), true);
return true;
}
catch (Exception ex)
@@ -67,8 +93,13 @@ public static class Marshal
try
{
FreeLibrary(_libraryHandle);
CodeFormatters.RegisterFormatter("Slang", typeof(PlainTextFormatter), true);
if (!FreeLibrary(_libraryHandle))
{
L.Warning("Unable to free Rust library");
}
_libraryHandle = IntPtr.Zero;
L.Debug("Rust DLL library freed");
return true;
}
catch (Exception ex)
@@ -78,11 +109,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())
{
compiledString = String.Empty;
sourceMapEntries = new();
return false;
}
@@ -95,19 +131,16 @@ public static class Marshal
};
var result = Ffi.compile_from_string(input);
try
{
if ((ulong)result.len < 1)
{
compiledString = String.Empty;
return false;
}
compiledString = result.AsString();
sourceMapEntries = result.source_map.ToList();
compiledString = result.output_code.AsString();
return true;
}
finally
{
result.Drop();
Ffi.free_ffi_compilation_result(result);
}
}
}
@@ -165,9 +198,9 @@ public static class Marshal
Assembly assembly = Assembly.GetExecutingAssembly();
using (Stream stream = assembly.GetManifestResourceStream(libName))
using (Stream resourceStream = assembly.GetManifestResourceStream(libName))
{
if (stream == null)
if (resourceStream == null)
{
L.Error(
$"{libName} not found. This means it was not embedded in the mod. Please contact the mod author!"
@@ -175,18 +208,85 @@ public static class Marshal
return "";
}
// Check if file exists and contents are identical to avoid overwriting locked files
if (File.Exists(destinationPath))
{
try
{
using (
FileStream fileStream = new FileStream(
destinationPath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite
)
)
{
if (resourceStream.Length == fileStream.Length)
{
if (StreamsContentsAreEqual(resourceStream, fileStream))
{
L.Debug(
$"DLL {libName} already exists and matches. Skipping extraction."
);
return destinationPath;
}
}
}
}
catch (IOException ex)
{
L.Warning(
$"Could not verify existing {libName}, attempting overwrite. {ex.Message}"
);
}
}
resourceStream.Position = 0;
// Attempt to overwrite if missing or different
try
{
using (FileStream fileStream = new FileStream(destinationPath, FileMode.Create))
{
stream.CopyTo(fileStream);
resourceStream.CopyTo(fileStream);
}
return destinationPath;
}
catch (IOException e)
{
L.Warning($"Could not overwrite {libName} (it might be in use): {e.Message}");
return "";
// If we fail here, the file is likely locked.
// However, if we are here, it means the file is DIFFERENT or we couldn't read it.
// As a fallback for live-reload, we can try returning the path anyway
// assuming the existing locked file might still work.
L.Warning(
$"Could not overwrite {libName} (it might be in use): {e.Message}. Attempting to use existing file."
);
return destinationPath;
}
}
}
private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2)
{
const int bufferSize = 4096;
byte[] buffer1 = new byte[bufferSize];
byte[] buffer2 = new byte[bufferSize];
while (true)
{
int count1 = stream1.Read(buffer1, 0, bufferSize);
int count2 = stream2.Read(buffer2, 0, bufferSize);
if (count1 != count2)
return false;
if (count1 == 0)
return true;
for (int i = 0; i < count1; i++)
{
if (buffer1[i] != buffer2[i])
return false;
}
}
}

View File

@@ -1,230 +1,11 @@
namespace Slang;
using System;
using Assets.Scripts.Objects;
using Assets.Scripts.Objects.Electrical;
using Assets.Scripts.Objects.Motherboards;
using Assets.Scripts.UI;
using HarmonyLib;
[HarmonyPatch]
public static class SlangPatches
{
private static ProgrammableChipMotherboard? _currentlyEditingMotherboard;
private static AsciiString? _motherboardCachedCode;
[HarmonyPatch(
typeof(ProgrammableChipMotherboard),
nameof(ProgrammableChipMotherboard.InputFinished)
)]
[HarmonyPrefix]
public static void pgmb_InputFinished(ref string result)
{
_currentlyEditingMotherboard = null;
_motherboardCachedCode = null;
// guard to ensure we have valid IC10 before continuing
if (
!SlangPlugin.IsSlangSource(ref result)
|| !Marshal.CompileFromString(result, out string compiled)
|| string.IsNullOrEmpty(compiled)
)
{
return;
}
var thisRef = Guid.NewGuid();
// Ensure we cache this compiled code for later retreival.
GlobalCode.SetSource(thisRef, result);
// Append REF to the bottom
compiled += $"\n{GlobalCode.SLANG_REF}{thisRef}";
result = compiled;
}
[HarmonyPatch(typeof(ProgrammableChipMotherboard), nameof(ProgrammableChipMotherboard.OnEdit))]
[HarmonyPrefix]
public static void isc_OnEdit(ProgrammableChipMotherboard __instance)
{
_currentlyEditingMotherboard = __instance;
_motherboardCachedCode = __instance.GetSourceCode();
var sourceCode = System.Text.Encoding.UTF8.GetString(
System.Text.Encoding.ASCII.GetBytes(__instance.GetSourceCode())
);
if (string.IsNullOrEmpty(sourceCode))
{
return;
}
// Look for REF at the bottom
var tagIndex = sourceCode.LastIndexOf(GlobalCode.SLANG_REF);
if (tagIndex == -1)
{
// this is not slang managed code
return;
}
if (
!Guid.TryParse(
sourceCode.Substring(tagIndex + GlobalCode.SLANG_REF.Length).Trim(),
out Guid sourceRef
)
)
{
// not a valid Guid, not managed by slang
return;
}
var slangSource = GlobalCode.GetSource(sourceRef);
if (string.IsNullOrEmpty(slangSource))
{
// Didn't find that source ref in the global code manager.
return;
}
__instance.SetSourceCode(slangSource);
}
private static void HandleSerialization(ref string sourceCode)
{
if (string.IsNullOrEmpty(sourceCode))
return;
// Check if the file ends with the Reference Tag
var tagIndex = sourceCode.LastIndexOf(GlobalCode.SLANG_REF);
if (tagIndex == -1)
return;
string guidString = sourceCode.Substring(tagIndex + GlobalCode.SLANG_REF.Length).Trim();
if (!Guid.TryParse(guidString, out Guid slangRefGuid))
{
L.Warning($"Found SLANG_REF but failed to parse GUID: {guidString}");
return;
}
var slangEncoded = GlobalCode.GetEncoded(slangRefGuid);
if (string.IsNullOrEmpty(slangEncoded))
{
L.Warning(
$"Could not find encoded source for ref {slangRefGuid}. Save will contain compiled IC10 only."
);
return;
}
// Extract the clean IC10 code (everything before the tag)
var cleanIc10 = sourceCode.Substring(0, tagIndex).TrimEnd();
// Append the encoded source tag to the bottom
sourceCode = $"{cleanIc10}\n{GlobalCode.SLANG_SRC}{slangEncoded}";
}
[HarmonyPatch(typeof(ProgrammableChip), nameof(ProgrammableChip.SerializeSave))]
[HarmonyPostfix]
public static void pgc_SerializeSave(ProgrammableChip __instance, ref ThingSaveData __result)
{
if (__result is not ProgrammableChipSaveData chipData)
return;
string code = chipData.SourceCode;
HandleSerialization(ref code);
chipData.SourceCode = code;
}
[HarmonyPatch(
typeof(ProgrammableChipMotherboard),
nameof(ProgrammableChipMotherboard.SerializeSave)
)]
[HarmonyPostfix]
public static void pgmb_SerializeSave(
ProgrammableChipMotherboard __instance,
ref ThingSaveData __result
)
{
if (__result is not ProgrammableChipMotherboardSaveData chipData)
return;
string code = chipData.SourceCode;
HandleSerialization(ref code);
chipData.SourceCode = code;
}
private static void HandleDeserialization(ref string sourceCode)
{
// Safety check for null/empty code
if (string.IsNullOrEmpty(sourceCode))
return;
// Check for the #SLANG_SRC: footer
int tagIndex = sourceCode.LastIndexOf(GlobalCode.SLANG_SRC);
// If the tag is missing, this is just a normal IC10 script. Do nothing.
if (tagIndex == -1)
return;
// Extract the Encoded Source (Base64)
string encodedSource = sourceCode.Substring(tagIndex + GlobalCode.SLANG_SRC.Length).Trim();
// Extract the IC10 Code (strip off the tag and the newline before it)
string ic10Code = sourceCode.Substring(0, tagIndex).TrimEnd();
// Generate a new Runtime GUID for this session
Guid runtimeGuid = Guid.NewGuid();
// Hydrate the Cache
GlobalCode.SetEncoded(runtimeGuid, encodedSource);
// Rewrite the SourceCode to the "Runtime" format (REF at bottom)
sourceCode = $"{ic10Code}\n{GlobalCode.SLANG_REF}{runtimeGuid}";
}
[HarmonyPatch(typeof(ProgrammableChip), nameof(ProgrammableChip.DeserializeSave))]
[HarmonyPrefix]
public static void pgc_DeserializeSave(ref ThingSaveData savedData)
{
if (savedData is not ProgrammableChipSaveData pcSaveData)
return;
string code = pcSaveData.SourceCode;
HandleDeserialization(ref code);
pcSaveData.SourceCode = code;
}
[HarmonyPatch(
typeof(ProgrammableChipMotherboard),
nameof(ProgrammableChipMotherboard.DeserializeSave)
)]
[HarmonyPrefix]
public static void pgmb_DeserializeSave(ref ThingSaveData savedData)
{
if (savedData is not ProgrammableChipMotherboardSaveData pcSaveData)
return;
string code = pcSaveData.SourceCode;
HandleDeserialization(ref code);
pcSaveData.SourceCode = code;
}
[HarmonyPatch(typeof(InputSourceCode), nameof(InputSourceCode.ButtonInputCancel))]
[HarmonyPrefix]
public static void isc_ButtonInputCancel()
{
if (_currentlyEditingMotherboard is null || _motherboardCachedCode is null)
{
return;
}
_currentlyEditingMotherboard.SetSourceCode(_motherboardCachedCode);
_currentlyEditingMotherboard = null;
_motherboardCachedCode = null;
}
[HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))]
[HarmonyPostfix]
public static void Stationpedia_Regenerate()

View File

@@ -1,7 +1,5 @@
using System.Text.RegularExpressions;
using BepInEx;
using HarmonyLib;
using LaunchPadBooster;
namespace Slang
{
@@ -41,45 +39,32 @@ namespace Slang
{
public const string PluginGuid = "com.biddydev.slang";
public const string PluginName = "Slang";
public const string PluginVersion = "0.1.1";
public const string PluginVersion = "0.5.0";
public static Mod MOD = new Mod(PluginName, PluginVersion);
private static Harmony? _harmony;
private Harmony? _harmony;
private static Regex? _slangSourceCheck = null;
private static Regex SlangSourceCheck
{
get
{
if (_slangSourceCheck is null)
{
_slangSourceCheck = new Regex(@"[;{}()]|\b(let|fn|device)\b|\/\/");
}
return _slangSourceCheck;
}
}
public static bool IsSlangSource(ref string input)
{
return SlangSourceCheck.IsMatch(input);
}
private void Awake()
public void Awake()
{
L.SetLogger(Logger);
this._harmony = new Harmony(PluginGuid);
_harmony = new Harmony(PluginGuid);
// If we failed to load the compiler, bail from the rest of the patches. It won't matter,
// as the compiler itself has failed to load.
if (!Marshal.Init())
{
L.Error("Marshal failed to init");
return;
}
this._harmony.PatchAll();
_harmony.PatchAll();
L.Debug("Ran Harmony patches");
}
public void OnDestroy()
{
Marshal.Destroy();
_harmony?.UnpatchSelf();
L.Debug("Cleaned up Harmony patches");
}
}
}

View File

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

View File

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

302
docs/builtins.md Normal file
View File

@@ -0,0 +1,302 @@
# Built-in Functions
<!--toc:start-->
- [Built-in Functions](#built-in-functions)
- [System Functions](#system-functions)
- [`yield()`](#yield)
- [`sleep(ticks)`](#sleepticks)
- [`hash(prefabName)`](#hashprefabname)
- [Device I/O Functions](#device-io-functions)
- [Reading from Devices](#reading-from-devices)
- [Load from device](#load-from-device)
- [Load From Device Batched](#load-from-device-batched)
- [Load From Device Batched Named](#load-from-device-batched-named)
- [Load Slot](#load-slot)
- [Load Reagent](#load-reagent)
- [Writing to Devices](#writing-to-devices)
- [Set On Device](#set-on-device)
- [Set On Device Batched](#set-on-device-batched)
- [Set On Device Batched Named](#set-on-device-batched-named)
- [Set Slot](#set-slot)
- [Math Functions](#math-functions)
- [Trigonometric Functions](#trigonometric-functions)
- [Trig Example](#trig-example)
- [Rounding Functions](#rounding-functions)
- [Rounding Example](#rounding-example)
- [Other Math Functions](#other-math-functions)
- [Math Example](#math-example)
- [See Also](#see-also)
<!--toc:end-->
Slang provides built-in functions for device I/O and mathematical operations.
These map directly to IC10 instructions.
## System Functions
### `yield()`
Pauses execution for exactly one game tick.
```rust
yield();
```
**IC10:** `yield`
---
### `sleep(ticks)`
Pauses execution for the specified number of ticks.
```rust
sleep(10); // Sleep for 10 ticks
```
**IC10:** `sleep ticks`
---
### `hash(prefabName)`
Computes the in-game hash for a prefab name. The hash is computed at compile
time and no runtime code is generated.
```rust
const AC_HASH = hash("StructureAirConditioner");
```
**Note:** This is different from IC10's `hash` instruction, which computes the
hash at runtime.
```rust
setBatched(AC_HASH, "On", 0);
```
**IC10:** `sb -2087593337 On 0` (no hash computation at runtime)
---
## Device I/O Functions
### Reading from Devices
#### Load from device
`load(device, property)` / `l(device, property)`
Loads a property value from a device:
```rust
let temp = load(sensor, "Temperature");
let temp = l(sensor, "Temperature");
// Preferred: use dot notation
let temp = sensor.Temperature;
```
**IC10:** `l r? d? var`
---
#### Load From Device Batched
`loadBatched(deviceHash, property, batchMode)` / `lb(...)`
Loads a property from all devices matching a hash, aggregated by batch mode:
```rust
const SENSOR = hash("StructureGasSensor");
let avgTemp = loadBatched(SENSOR, "Temperature", "Average");
let maxTemp = lb(SENSOR, "Temperature", "Maximum");
```
**Batch Modes:** `"Average"`, `"Sum"`, `"Minimum"`, `"Maximum"`
**IC10:** `lb r? deviceHash logicType batchMode`
---
#### Load From Device Batched Named
`loadBatchedNamed(deviceHash, nameHash, property, batchMode)` / `lbn(...)`
Loads a property from devices matching both device hash and name hash:
```rust
const SENSOR_HASH = hash("StructureGasSensor");
const SENSOR_NAME_HASH = hash("Outdoor Gas Sensor");
let avgTemp = loadBatchedNamed(SENSOR_HASH, SENSOR_NAME_HASH, "Temperature", "Average");
let maxTemp = lbn(SENSOR_HASH, SENSOR_NAME_HASH, "Temperature", "Maximum");
```
**IC10:** `lbn r? deviceHash nameHash logicType batchMode`
**Note:** This function is useful when a script interfaces with a lot of
devices, as it allows for arbitrary device access without limited to the 6 `dx` pins.
---
#### Load Slot
`loadSlot(device, slotIndex, property)` / `ls(...)`
Loads a slot property from a device:
```rust
let occupied = loadSlot(sorter, 0, "Occupied");
let occupied = ls(sorter, 0, "Occupied");
```
**IC10:** `ls r? d? slotIndex logicSlotType`
---
#### Load Reagent
`loadReagent(device, reagentMode, reagentHash)` / `lr(...)`
Loads reagent information from a device:
```rust
let amount = loadReagent(furnace, "Contents", reagentHash);
let amount = lr(furnace, "Contents", reagentHash);
```
**IC10:** `lr r? d? reagentMode reagentHash`
---
### Writing to Devices
#### Set On Device
`set(device, property, value)` / `s(...)`
Sets a property on a device:
```rust
set(valve, "On", true);
s(valve, "On", true);
// Preferred: use dot notation
valve.On = true;
```
**IC10:** `s d? logicType r?`
---
#### Set On Device Batched
`setBatched(deviceHash, property, value)` / `sb(...)`
Sets a property on all devices matching a hash:
```rust
const LIGHT_HASH = hash("StructureWallLight");
setBatched(LIGHT_HASH, "On", true);
sb(LIGHT_HASH, "On", true);
```
**IC10:** `sb deviceHash logicType r?`
**Note:** This function is useful when a script interfaces with a lot of devices,
as it allows for arbitrary device access without limited to the 6 `dx` pins.
---
#### Set On Device Batched Named
`setBatchedNamed(deviceHash, nameHash, property, value)` / `sbn(...)`
Sets a property on devices matching both device hash and name hash:
```rust
const SENSOR_HASH = hash("StructureGasSensor");
const SENSOR_NAME_HASH = hash("Outdoor Gas Sensor");
setBatchedNamed(SENSOR_HASH, SENSOR_NAME_HASH, "On", true);
sbn(SENSOR_HASH, SENSOR_NAME_HASH, "On", true);
```
**IC10:** `sbn deviceHash nameHash logicType r?`
---
#### Set Slot
`setSlot(device, slotIndex, property, value)` / `ss(...)`
Sets a slot property on a device:
```rust
setSlot(sorter, 0, "Open", true);
ss(sorter, 0, "Open", true);
```
**IC10:** `ss d? slotIndex logicSlotType r?`
---
## Math Functions
All math functions accept numbers, variables, or expressions as arguments.
### Trigonometric Functions
| Function | Description | IC10 |
| ------------- | ---------------------------- | ------- |
| `sin(x)` | Sine of angle in radians | `sin` |
| `cos(x)` | Cosine of angle in radians | `cos` |
| `tan(x)` | Tangent of angle in radians | `tan` |
| `asin(x)` | Arc sine, returns radians | `asin` |
| `acos(x)` | Arc cosine, returns radians | `acos` |
| `atan(x)` | Arc tangent, returns radians | `atan` |
| `atan2(y, x)` | Two-argument arc tangent | `atan2` |
#### Trig Example
```rust
let angle = atan2(y, x);
let sineValue = sin(angle);
```
### Rounding Functions
| Function | Description | IC10 |
| ---------- | ----------------------------- | ------- |
| `ceil(x)` | Round up to nearest integer | `ceil` |
| `floor(x)` | Round down to nearest integer | `floor` |
| `trunc(x)` | Remove decimal portion | `trunc` |
| `abs(x)` | Absolute value | `abs` |
#### Rounding Example
```rust
let rounded = floor(3.7); // 3
let positive = abs(-5); // 5
```
### Other Math Functions
| Function | Description | IC10 |
| ----------- | ----------------------------- | ------ |
| `sqrt(x)` | Square root | `sqrt` |
| `log(x)` | Natural logarithm | `log` |
| `max(a, b)` | Maximum of two values | `max` |
| `min(a, b)` | Minimum of two values | `min` |
| `rand()` | Random number between 0 and 1 | `rand` |
#### Math Example
```rust
let root = sqrt(16); // 4
let bigger = max(a, b);
let randomVal = rand();
```
## See Also
- [Language Reference](language-reference.md) — Complete syntax guide
- [Examples](examples.md) — Real-world code samples

254
docs/examples.md Normal file
View File

@@ -0,0 +1,254 @@
# Examples
Real-world Slang programs demonstrating common patterns.
## Temperature Control
Basic thermostat that controls an air conditioner based on room temperature:
```rust
device ac = "db";
device roomGasSensor = "d0";
const TARGET_TEMP = 22c;
const HYSTERESIS = 1;
loop {
yield();
let temp = roomGasSensor.Temperature;
if (temp > TARGET_TEMP + HYSTERESIS) {
ac.On = true;
} else if (temp < TARGET_TEMP - HYSTERESIS) {
ac.On = false;
}
}
```
**Note:** The IC10 chip is assumed to be inserted in the air conditioner's IC slot.
---
## Two-Axis Solar Panel Tracking
Handles two-axis solar panel tracking based on the sun's position:
```rust
device sensor = "d0";
const H_PANELS = hash("StructureSolarPanelDual");
loop {
setBatched(H_PANELS, "Horizontal", sensor.Horizontal);
setBatched(H_PANELS, "Vertical", sensor.Vertical + 90);
yield();
}
```
**Note:** Assumes the daylight sensor is mounted with its port looking 90
degrees east of the solar panel's data port, an offset can be added on the
horizontal angle if needed.
---
## Day/Night Lighting
Controls grow lights during the day and ambient lights at night:
```rust
device greenhouseSensor = "d0";
const daylightSensor = hash("StructureDaylightSensor");
const growLight = hash("StructureGrowLight");
const wallLight = hash("StructureLightLong");
loop {
yield();
let solarAngle = lb(daylightSensor, "SolarAngle", "Average");
let isDaylight = solarAngle < 90;
sb(growLight, "On", isDaylight);
sb(wallLight, "On", !isDaylight);
}
```
---
## Pressure Relief Valve
Controls a volume pump based on pressure readings for emergency pressure relief:
```rust
device volumePump = "d0";
device pipeSensor = "d1";
const MAX_PRESSURE = 10_000;
const R = 8.314;
loop {
yield();
let pressure = pipeSensor.Pressure;
if (pressure > MAX_PRESSURE) {
// Use PV=nRT to calculate the amount of mols we need to move
// n = PV / RT
let molsToMove = (pressure - MAX_PRESSURE) *
pipeSensor.Volume / (R * pipeSensor.Temperature);
// V = nRT / P
let setting = molsToMove * R * pipeSensor.Temperature / pressure;
volumePump.Setting = setting;
volumePump.On = true;
} else {
volumePump.On = false;
}
}
```
---
## Greenhouse Environment Controller
Complete greenhouse control with pressure, temperature, and lighting:
```rust
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 {
yield();
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);
}
```
---
## Advanced Furnace Pressure Control
Automates multi-furnace pump control based on dial setting for pressure target:
```rust
const FURNACE1 = 1234;
const DIAL1 = 1123;
const ANALYZER1 = 1223;
const FURNACE2 = 1235;
const DIAL2 = 1124;
const ANALYZER2 = 1224;
const FURNACE3 = 1236;
const DIAL3 = 1124;
const ANALYZER3 = 1225;
const R = 8.314;
fn handleFurnace(furnace, dial, analyzer) {
let pressure = furnace.Pressure;
let targetPressure = max(dial.Setting, 0.1) * 1000;
if (abs(targetPressure - pressure) <= 0.1) {
furnace.On = false;
return;
}
let molsToMove = max(furnace.TotalMoles, 1) * (
(targetPressure / pressure) - 1
);
// V = nRT / P
if (molsToMove > 0) {
// Calculate volume required
if (analyzer.Pressure == 0) {
// No more gas to add
furnace.On = false;
return;
}
let volume = molsToMove * R * analyzer.Temperature / analyzer.Pressure;
furnace.On = true;
furnace.SettingOutput = 0;
furnace.SettingInput = volume;
return;
}
// Calculate volume required
let volume = (-molsToMove) * R * furnace.Temperature / pressure;
furnace.On = true;
furnace.SettingInput = 0;
furnace.SettingOutput = volume;
return;
}
loop {
yield();
handleFurnace(FURNACE1, DIAL1, ANALYZER1);
handleFurnace(FURNACE2, DIAL2, ANALYZER2);
handleFurnace(FURNACE3, DIAL3, ANALYZER3);
}
```
**Note:** This example does not handle edge cases such as insufficient gas in
the input network or overfilling the furnace/pipe network.
---
## Common Patterns
### Waiting for a Condition
```rust
fn waitForDeviceToTurnOff(device) {
while (device.On) {
yield();
}
}
```
## See Also
- [Getting Started](getting-started.md) — First steps with Slang
- [Language Reference](language-reference.md) — Complete syntax guide
- [Built-in Functions](builtins.md) — System calls and math functions

99
docs/getting-started.md Normal file
View File

@@ -0,0 +1,99 @@
# Getting Started
<!--toc:start-->
- [Getting Started](#getting-started)
- [Program Structure](#program-structure)
- [The `yield()` Function](#the-yield-function)
- [Your First Program](#your-first-program)
- [Explanation](#explanation)
- [Comments](#comments)
- [See Also](#see-also)
<!--toc:end-->
This guide covers the basics of writing your first Slang program.
## Program Structure
A Slang program consists of top-level declarations and a main loop:
```rust
// Device declarations
device self = "db";
device sensor = "d0";
// Constants
const THRESHOLD = 100;
// Variables
let counter = 0;
// Main program loop
loop {
yield();
// Your logic here
}
```
## The `yield()` Function
IC10 programs run continuously. The `yield()` function pauses execution for one
game tick, preventing the script from consuming excessive resources.
**Important:** You should always include `yield()` in your main loop unless you
know what you're doing.
```rust
loop {
yield(); // Recommended!
// ...
}
```
## Your First Program
Here's a simple program that turns on a light when a gas sensor detects low
pressure:
```rust
device gasSensor = "d0";
device light = "d1";
const LOW_PRESSURE = 50;
loop {
yield();
light.On = gasSensor.Pressure < LOW_PRESSURE;
}
```
### Explanation
1. `device gasSensor = "d0"` — Binds the device at port `d0` to the name
`gasSensor`
2. `device light = "d1"` — Binds the device at port `d1` to the name `light`
3. `const LOW_PRESSURE = 50` — Defines a compile-time constant
4. `loop { ... }` — Creates an infinite loop
5. `yield()` — Pauses for one tick
6. `light.On = gasSensor.Pressure < LOW_PRESSURE` — Reads the pressure and sets
the light state
## Comments
Slang supports single-line comments and documentation comments:
```rust
// This is a regular comment
/// This is a documentation comment
/// It can span multiple lines
fn myFunction() {
// ...
}
```
## See Also
- [Language Reference](language-reference.md) — Complete syntax guide
- [Built-in Functions](builtins.md) — Available system calls
- [Examples](examples.md) — Real-world programs and patterns

339
docs/language-reference.md Normal file
View File

@@ -0,0 +1,339 @@
# Language Reference
<!--toc:start-->
- [Language Reference](#language-reference)
- [Literals](#literals)
- [Numbers](#numbers)
- [Temperature Literals](#temperature-literals)
- [Booleans](#booleans)
- [Strings](#strings)
- [Variables](#variables)
- [`let` - Mutable Variables](#let-mutable-variables)
- [`const` - Constants](#const-constants)
- [Device Declarations](#device-declarations)
- [Device Property Access](#device-property-access)
- [Device Property Assignment](#device-property-assignment)
- [Operators](#operators)
- [Arithmetic Operators](#arithmetic-operators)
- [Comparison Operators](#comparison-operators)
- [Logical Operators](#logical-operators)
- [Ternary Operator](#ternary-operator)
- [Operator Precedence](#operator-precedence)
- [Control Flow](#control-flow)
- [`if` / `else`](#if-and-else)
- [`loop`](#loop)
- [`while`](#while)
- [`break`](#break)
- [`continue`](#continue)
- [Functions](#functions)
- [Declaration](#declaration)
- [Invocation](#invocation)
- [Return Values](#return-values)
- [Parentheses for Grouping](#parentheses-for-grouping)
- [See Also](#see-also)
<!--toc:end-->
Complete syntax reference for the Slang programming language.
## Literals
### Numbers
Numbers can be integers or decimals. Underscores are allowed as visual
separators:
```rust
const integer = 42; // Integer
const decimal = 3.14; // Decimal
const million = 1_000_000; // Integer with separators
const decimalSeparators = 5_000.50; // Decimal with separators
```
### Temperature Literals
Append a unit suffix to specify temperature. Values are automatically converted
to Kelvin at compile time:
| Suffix | Unit | Example |
| ------ | ---------- | ------- |
| `c` | Celsius | `20c` |
| `f` | Fahrenheit | `68f` |
| `k` | Kelvin | `293k` |
```rust
const ROOM_TEMP = 20c; // Converts to 293.15 Kelvin
const FREEZING = 32f; // Converts to 273.15 Kelvin
const ABSOLUTE1 = 0k; // Already in Kelvin
const ABSOLUTE2 = 0; // Assumed to be in Kelvin
```
### Booleans
Booleans compile to integer values `1` and `0` in IC10.
```rust
device ac = "d0";
ac.Mode = false;
ac.On = true;
```
### Strings
Strings use double or single quotes. They are primarily used for prefab and
name hashes.
```rust
const AC_HASH = hash("StructureAirConditioner");
const AC_NAME_HASH = hash("Greenhouse Air Conditioner");
```
## Variables
### `let` Mutable Variables
Declares a variable that can be reassigned:
```rust
let counter = 0;
// ...
counter = counter + 1;
```
### `const` Constants
Declares a compile-time constant. Constants are inlined and do not consume
registers:
```rust
const MAX_PRESSURE = 10_000;
const DOOR_HASH = hash("StructureCompositeDoor");
```
Constants support the `hash()` function for compile-time hash computation.
## Device Declarations
The `device` keyword binds a device port or reference ID to a named variable:
```rust
device self = "db"; // IC housing, or device the IC is plugged into (eg. an AC)
device sensor = "d0"; // Device at port d0
device valve = "d1"; // Device at port d1
device ac1 = "$3FC"; // Device with reference ID $3FC (hexadecimal 1020)
device ac2 = "1020"; // Device with reference ID 1020 (decimal)
```
**Note:** Reference IDs can be found in-game using the Configuration cartridge.
### Device Property Access
Read device properties using dot notation:
```rust
let temp = sensor.Temperature;
let pressure = sensor.Pressure;
let isOn = valve.On;
```
### Device Property Assignment
Write to device properties using dot notation:
```rust
valve.On = true;
valve.Setting = 100;
```
## Operators
### Arithmetic Operators
| Operator | Description | Example |
| -------- | -------------- | -------- |
| `+` | Addition | `a + b` |
| `-` | Subtraction | `a - b` |
| `*` | Multiplication | `a * b` |
| `/` | Division | `a / b` |
| `%` | Modulo | `a % b` |
| `**` | Exponentiation | `a ** b` |
| `-` | Negation | `-a` |
### Comparison Operators
| Operator | Description | Example |
| -------- | --------------------- | -------- |
| `==` | Equal | `a == b` |
| `!=` | Not equal | `a != b` |
| `<` | Less than | `a < b` |
| `>` | Greater than | `a > b` |
| `<=` | Less than or equal | `a <= b` |
| `>=` | Greater than or equal | `a >= b` |
### Logical Operators
| Operator | Description | Example |
| -------- | ----------- | ---------- |
| `&&` | Logical AND | `a && b` |
| `\|\|` | Logical OR | `a \|\| b` |
| `!` | Logical NOT | `!a` |
### Ternary Operator
Conditional expressions using `?` and `:`:
```rust
let result = condition ? valueIfTrue : valueIfFalse;
```
### Operator Precedence
Operators are evaluated in the following order, from highest to lowest
precedence:
| Precedence | Operator(s) | Description |
| ---------- | ----------------- | -------------------------------- |
| 1 | `()` `.` | Grouping, Property access |
| 2 | `!` `-` | Logical NOT, Negation |
| 3 | `**` | Exponentiation |
| 4 | `*` `/` `%` | Multiplication, Division, Modulo |
| 5 | `+` `-` | Addition, Subtraction |
| 6 | `<` `<=` `>` `>=` | Comparison |
| 7 | `==` `!=` | Equality |
| 8 | `&&` | Logical AND |
| 9 | `\|\|` | Logical OR |
| 10 | `?:` | Ternary conditional |
| 11 | `=` | Assignment |
Use parentheses to override precedence:
```rust
let result = (20 + 10) * 5;
```
## Control Flow
### if and else
Conditional branching:
```rust
if (tank.Temperature > 30c) {
ac.On = true;
} else {
ac.On = false;
}
```
### `loop`
Infinite loop that runs until `break`:
```rust
loop {
yield();
// Loop body
if (condition) {
break; // Exit the loop
}
}
```
### `while`
Conditional loop that runs while the condition is true:
```rust
while (counter < 100) {
counter = counter + 1;
yield();
}
```
### `break`
Exits the current loop:
```rust
loop {
yield();
// ...
if (done) {
break;
}
}
```
### `continue`
Skips to the next iteration of the current loop:
```rust
loop {
yield();
if (shouldSkip) {
continue;
}
// This code is skipped when shouldSkip is true
// ...
}
```
## Functions
**Warning:** Functions are currently experimental and may produce suboptimal code.
### Declaration
```rust
fn functionName(arg1, arg2) {
// Function body
return arg1 + arg2;
}
```
### Invocation
```rust
let result = functionName(10, 20);
```
### Return Values
Use `return` to exit a function and optionally return a value:
```rust
fn calculate(x) {
if (x < 0) {
return 0; // Early return
}
return x * 2;
}
fn doWork() {
// No return value
return;
}
```
## Parentheses for Grouping
Use parentheses to control operator precedence:
```rust
let result = (a + b) * c;
let complex = (
temp > 0c &&
stress < 50 &&
(pressure < 10_000 || temp > 20c)
);
```
## See Also
- [Getting Started](getting-started.md) — First steps with Slang
- [Built-in Functions](builtins.md) — System calls and math functions
- [Examples](examples.md) — Real-world code samples

View File

@@ -172,9 +172,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.19.0"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "bytecheck"
@@ -268,6 +268,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"helpers",
"il",
"indoc",
"lsp-types",
"parser",
@@ -397,6 +398,15 @@ name = "helpers"
version = "0.1.0"
dependencies = [
"crc32fast",
"lsp-types",
]
[[package]]
name = "il"
version = "0.1.0"
dependencies = [
"helpers",
"rust_decimal",
]
[[package]]
@@ -563,6 +573,16 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "optimizer"
version = "0.1.0"
dependencies = [
"anyhow",
"helpers",
"il",
"rust_decimal",
]
[[package]]
name = "parser"
version = "0.1.0"
@@ -571,6 +591,7 @@ dependencies = [
"helpers",
"lsp-types",
"pretty_assertions",
"safer-ffi",
"thiserror",
"tokenizer",
]
@@ -909,13 +930,14 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "slang"
version = "0.2.1"
version = "0.5.0"
dependencies = [
"anyhow",
"clap",
"compiler",
"helpers",
"lsp-types",
"optimizer",
"parser",
"rust_decimal",
"safer-ffi",
@@ -1041,18 +1063,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.3"
version = "0.7.4+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
checksum = "fe3cea6b2aa3b910092f6abd4053ea464fab5f9c170ba5e9a6aead16ec4af2b6"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.23.7"
version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap",
"toml_datetime",
@@ -1062,9 +1084,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.4"
version = "1.0.5+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
checksum = "4c03bee5ce3696f31250db0bbaff18bc43301ce0e8db2ed1f07cbb2acf89984c"
dependencies = [
"winnow",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "slang"
version = "0.2.1"
version = "0.5.0"
edition = "2021"
[workspace]
@@ -9,9 +9,10 @@ members = ["libs/*"]
[workspace.dependencies]
thiserror = "2"
rust_decimal = "1"
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
crc32fast = "1.5" # This is for `HASH(..)` calls to be optimized away
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
crc32fast = "1.5" # This is for `HASH(..)` calls to be optimized away
anyhow = { version = "^1.0", features = ["backtrace"] }
[features]
headers = ["safer-ffi/headers"]
@@ -42,7 +43,8 @@ tokenizer = { path = "libs/tokenizer" }
parser = { path = "libs/parser" }
compiler = { path = "libs/compiler" }
helpers = { path = "libs/helpers" }
optimizer = { path = "libs/optimizer" }
safer-ffi = { workspace = true }
anyhow = { version = "^1.0", features = ["backtrace"] }
anyhow = { workspace = true }
[dev-dependencies]

View File

@@ -8,6 +8,7 @@ thiserror = { workspace = true }
parser = { path = "../parser" }
tokenizer = { path = "../tokenizer" }
helpers = { path = "../helpers" }
il = { path = "../il" }
lsp-types = { workspace = true }
rust_decimal = { workspace = true }

View File

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

View File

@@ -1,23 +1,29 @@
use crate::compile;
use anyhow::Result;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn simple_binary_expression() -> anyhow::Result<()> {
let compiled = compile! {
debug
fn simple_binary_expression() -> Result<()> {
let result = compile! {
check
"
let i = 1 + 2;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 3 #i
move r8 3
"
}
);
@@ -26,9 +32,9 @@ fn simple_binary_expression() -> anyhow::Result<()> {
}
#[test]
fn nested_binary_expressions() -> anyhow::Result<()> {
let compiled = compile! {
debug
fn nested_binary_expressions() -> Result<()> {
let result = compile! {
check
"
fn calculateArgs(arg1, arg2, arg3) {
return (arg1 + arg2) * arg3;
@@ -38,31 +44,39 @@ fn nested_binary_expressions() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
calculateArgs:
pop r8 #arg3
pop r9 #arg2
pop r10 #arg1
pop r8
pop r9
pop r10
push sp
push ra
add r1 r10 r9
mul r2 r1 r8
move r15 r2
sub r0 sp 1
get ra db r0
sub sp sp 1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
push 10
push 20
push 30
jal calculateArgs
move r1 r15 #__binary_temp_3
move r1 r15
add r2 r1 100
move r8 r2 #returned
move r8 r2
"
}
);
@@ -71,21 +85,27 @@ fn nested_binary_expressions() -> anyhow::Result<()> {
}
#[test]
fn stress_test_constant_folding() -> anyhow::Result<()> {
let compiled = compile! {
debug
fn stress_test_constant_folding() -> Result<()> {
let result = compile! {
check
"
let negationHell = (-1 + -2) * (-3 + (-4 * (-5 + -6)));
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 -123 #negationHell
move r8 -123
"
}
);
@@ -94,17 +114,23 @@ fn stress_test_constant_folding() -> anyhow::Result<()> {
}
#[test]
fn test_constant_folding_with_variables_mixed_in() -> anyhow::Result<()> {
let compiled = compile! {
debug
fn test_constant_folding_with_variables_mixed_in() -> Result<()> {
let result = compile! {
check
r#"
device self = "db";
let i = 1 - 3 * (1 + 123.4) * self.Setting + 245c;
"#
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -113,7 +139,132 @@ fn test_constant_folding_with_variables_mixed_in() -> anyhow::Result<()> {
mul r2 373.2 r1
sub r3 1 r2
add r4 r3 518.15
move r8 r4 #i
move r8 r4
"
}
);
Ok(())
}
#[test]
fn test_ternary_expression() -> Result<()> {
let result = compile! {
check
r#"
let i = 1 > 2 ? 15 : 20;
"#
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
result.output,
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 result = compile! {
check
r#"
let i = 0;
i = 1 > 2 ? 15 : 20;
"#
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
result.output,
indoc! {
"
j main
main:
move r8 0
sgt r1 1 2
select r2 r1 15 20
move r8 r2
"
}
);
Ok(())
}
#[test]
fn test_negative_literals() -> Result<()> {
let result = compile!(
check
r#"
let item = -10c - 20c;
"#
);
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
result.output,
indoc! {
"
j main
main:
move r8 243.15
"
}
);
Ok(())
}
#[test]
fn test_mismatched_temperature_literals() -> Result<()> {
let result = compile!(
check
r#"
let item = -10c - 100k;
let item2 = item + 500c;
"#
);
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
result.output,
indoc! {
"
j main
main:
move r8 163.15
add r1 r8 773.15
move r9 r1
"
}
);

View File

@@ -1,11 +1,10 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_if_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 10;
if (a > 5) {
@@ -14,17 +13,23 @@ fn test_if_statement() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 10 #a
move r8 10
sgt r1 r8 5
beq r1 0 L1
move r8 20 #a
L1:
beqz r1 __internal_L1
move r8 20
__internal_L1:
"
}
);
@@ -34,8 +39,8 @@ fn test_if_statement() -> anyhow::Result<()> {
#[test]
fn test_if_else_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
if (10 > 5) {
@@ -46,20 +51,26 @@ fn test_if_else_statement() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 0 #a
move r8 0
sgt r1 10 5
beq r1 0 L2
move r8 1 #a
j L1
L2:
move r8 2 #a
L1:
beqz r1 __internal_L2
move r8 1
j __internal_L1
__internal_L2:
move r8 2
__internal_L1:
"
}
);
@@ -69,8 +80,8 @@ fn test_if_else_statement() -> anyhow::Result<()> {
#[test]
fn test_if_else_if_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
if (a == 1) {
@@ -83,26 +94,32 @@ fn test_if_else_if_statement() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 0 #a
move r8 0
seq r1 r8 1
beq r1 0 L2
move r8 10 #a
j L1
L2:
beqz r1 __internal_L2
move r8 10
j __internal_L1
__internal_L2:
seq r2 r8 2
beq r2 0 L4
move r8 20 #a
j L3
L4:
move r8 30 #a
L3:
L1:
beqz r2 __internal_L4
move r8 20
j __internal_L3
__internal_L4:
move r8 30
__internal_L3:
__internal_L1:
"
}
);
@@ -112,8 +129,8 @@ fn test_if_else_if_statement() -> anyhow::Result<()> {
#[test]
fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 1;
let b = 2;
@@ -130,25 +147,31 @@ fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 1 #a
move r9 2 #b
move r10 3 #c
move r11 4 #d
move r12 5 #e
move r13 6 #f
move r14 7 #g
push 8 #h
move r8 1
move r9 2
move r10 3
move r11 4
move r12 5
move r13 6
move r14 7
push 8
seq r1 r8 1
beq r1 0 L1
beqz r1 __internal_L1
sub r0 sp 1
put db r0 99 #h
L1:
put db r0 99
__internal_L1:
sub sp sp 1
"
}

View File

@@ -1,72 +1,93 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn no_arguments() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn doSomething() {};
let i = doSomething();
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
let to_test = indoc! {
"
j main
doSomething:
push sp
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
__internal_L1:
pop ra
pop sp
j ra
main:
jal doSomething
move r8 r15 #i
move r8 r15
"
};
assert_eq!(compiled, to_test);
assert_eq!(result.output, to_test);
Ok(())
}
#[test]
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! {
debug
let result = compile! {
check
"
fn doSomething(arg1) {};
let arg1 = 123;
let i = doSomething(arg1);
fn mul2(arg1) {
return arg1 * 2;
};
loop {
let arg1 = 123;
let i = mul2(arg1);
i = i ** 2;
}
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
doSomething:
pop r8 #arg1
mul2:
pop r8
push sp
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
mul r1 r8 2
move r15 r1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
move r8 123 #arg1
__internal_L2:
move r8 123
push r8
push r8
jal doSomething
sub r0 sp 1
get r8 db r0
sub sp sp 1
move r9 r15 #i
sub sp sp 1
jal mul2
pop r8
move r9 r15
pow r1 r9 2
move r9 r1
j __internal_L2
__internal_L3:
"
}
);
@@ -94,39 +115,47 @@ fn incorrect_args_count() -> anyhow::Result<()> {
#[test]
fn inline_literal_args() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn doSomething(arg1, arg2) {};
fn doSomething(arg1, arg2) {
return 5;
};
let thisVariableShouldStayInPlace = 123;
let returnedValue = doSomething(12, 34);
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
doSomething:
pop r8 #arg2
pop r9 #arg1
pop r8
pop r9
push sp
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
move r15 5
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
move r8 123 #thisVariableShouldStayInPlace
move r8 123
push r8
push 12
push 34
jal doSomething
sub r0 sp 1
get r8 db r0
sub sp sp 1
move r9 r15 #returnedValue
sub sp sp 1
pop r8
move r9 r15
"
}
);
@@ -136,8 +165,8 @@ fn inline_literal_args() -> anyhow::Result<()> {
#[test]
fn mixed_args() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let arg1 = 123;
let returnValue = doSomething(arg1, 456);
@@ -145,30 +174,34 @@ fn mixed_args() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
doSomething:
pop r8 #arg2
pop r9 #arg1
pop r8
pop r9
push sp
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
__internal_L1:
pop ra
pop sp
j ra
main:
move r8 123 #arg1
move r8 123
push r8
push r8
push 456
jal doSomething
sub r0 sp 1
get r8 db r0
sub sp sp 1
move r9 r15 #returnValue
sub sp sp 1
pop r8
move r9 r15
"
}
);
@@ -178,8 +211,8 @@ fn mixed_args() -> anyhow::Result<()> {
#[test]
fn with_return_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn doSomething(arg1) {
return 456;
@@ -189,23 +222,31 @@ fn with_return_statement() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
doSomething:
pop r8 #arg1
pop r8
push sp
push ra
move r15 456 #returnValue
sub r0 sp 1
get ra db r0
sub sp sp 1
move r15 456
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
push 123
jal doSomething
move r8 r15 #returned
move r8 r15
"
}
);
@@ -215,8 +256,8 @@ fn with_return_statement() -> anyhow::Result<()> {
#[test]
fn with_negative_return_literal() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn doSomething() {
return -1;
@@ -225,21 +266,28 @@ fn with_negative_return_literal() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
doSomething:
push sp
push ra
move r15 -1 #returnValue
sub r0 sp 1
get ra db r0
sub sp sp 1
move r15 -1
__internal_L1:
pop ra
pop sp
j ra
main:
jal doSomething
move r8 r15 #i
move r8 r15
"
}
);

View File

@@ -4,18 +4,24 @@ use pretty_assertions::assert_eq;
#[test]
fn variable_declaration_numeric_literal() -> anyhow::Result<()> {
let compiled = crate::compile! {
debug r#"
check r#"
let i = 20c;
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
move r8 293.15 #i
move r8 293.15
"
}
);
@@ -26,7 +32,7 @@ fn variable_declaration_numeric_literal() -> anyhow::Result<()> {
#[test]
fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
let a = 0;
let b = 1;
@@ -40,22 +46,28 @@ fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()>
let j = 9;
"#};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
move r8 0 #a
move r9 1 #b
move r10 2 #c
move r11 3 #d
move r12 4 #e
move r13 5 #f
move r14 6 #g
push 7 #h
push 8 #i
push 9 #j
move r8 0
move r9 1
move r10 2
move r11 3
move r12 4
move r13 5
move r14 6
push 7
push 8
push 9
sub sp sp 3
"
}
@@ -67,19 +79,25 @@ fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()>
#[test]
fn variable_declaration_negative() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
let i = -1;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
move r8 -1 #i
move r8 -1
"
}
);
@@ -90,21 +108,27 @@ fn variable_declaration_negative() -> anyhow::Result<()> {
#[test]
fn test_boolean_declaration() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
let t = true;
let f = false;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
move r8 1 #t
move r9 0 #f
move r8 1
move r9 0
"
}
);
@@ -115,31 +139,39 @@ fn test_boolean_declaration() -> anyhow::Result<()> {
#[test]
fn test_boolean_return() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
fn getTrue() {
return true;
};
let val = getTrue();
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
getTrue:
push sp
push ra
move r15 1 #returnValue
sub r0 sp 1
get ra db r0
sub sp sp 1
move r15 1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
jal getTrue
move r8 r15 #val
move r8 r15
"
}
);
@@ -149,15 +181,21 @@ fn test_boolean_return() -> anyhow::Result<()> {
#[test]
fn test_const_hash_expr() -> anyhow::Result<()> {
let compiled = compile!(debug r#"
let compiled = compile!(check r#"
const nameHash = hash("AccessCard");
device self = "db";
self.Setting = nameHash;
"#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -168,3 +206,34 @@ fn test_const_hash_expr() -> anyhow::Result<()> {
);
Ok(())
}
#[test]
fn test_declaration_is_const() -> anyhow::Result<()> {
let compiled = compile! {
check
r#"
const MAX = 100;
let max = MAX;
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 100
"
}
);
Ok(())
}

View File

@@ -0,0 +1,274 @@
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn device_declaration() -> anyhow::Result<()> {
let compiled = compile! {
check "
device d0 = \"d0\";
"
};
// Declaration only emits the jump label header
assert_eq!(compiled.output, "j main\n");
Ok(())
}
#[test]
fn device_property_read() -> anyhow::Result<()> {
let compiled = compile! {
check "
device ac = \"d0\";
let temp = ac.Temperature;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
l r1 d0 Temperature
move r8 r1
"
}
);
Ok(())
}
#[test]
fn device_property_write() -> anyhow::Result<()> {
let compiled = compile! {
check "
device ac = \"d0\";
ac.On = 1;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
s d0 On 1
"
}
);
Ok(())
}
#[test]
fn multiple_device_declarations() -> anyhow::Result<()> {
let compiled = compile! {
check "
device d0 = \"d0\";
device d1 = \"d1\";
device d2 = \"d2\";
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
// Declarations only emit the header when unused
assert_eq!(compiled.output, "j main\n");
Ok(())
}
#[test]
fn device_with_variable_interaction() -> anyhow::Result<()> {
let compiled = compile! {
check "
device sensor = \"d0\";
let reading = sensor.Temperature;
let threshold = 373.15;
let alert = reading > threshold;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
l r1 d0 Temperature
move r8 r1
move r9 373.15
sgt r2 r8 r9
move r10 r2
"
}
);
Ok(())
}
#[test]
fn device_property_in_arithmetic() -> anyhow::Result<()> {
let compiled = compile! {
check "
device d0 = \"d0\";
let result = d0.Temperature + 100;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
// Verify that we load property, add 100, and move to result
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
l r1 d0 Temperature
add r2 r1 100
move r8 r2
"
}
);
Ok(())
}
#[test]
fn device_used_in_function() -> anyhow::Result<()> {
let compiled = compile! {
check "
device d0 = \"d0\";
fn check_power() {
return d0.On;
};
let powered = check_power();
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
check_power:
push sp
push ra
l r1 d0 On
move r15 r1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
jal check_power
move r8 r15
"
}
);
Ok(())
}
#[test]
fn device_in_conditional() -> anyhow::Result<()> {
let compiled = compile! {
check "
device d0 = \"d0\";
if (d0.On) {
let x = 1;
}
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
l r1 d0 On
beqz r1 __internal_L1
move r8 1
__internal_L1:
"
}
);
Ok(())
}
#[test]
fn device_property_with_underscore_name() -> anyhow::Result<()> {
let compiled = compile! {
check "
device cool_device = \"d0\";
let value = cool_device.SomeProperty;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
l r1 d0 SomeProperty
move r8 r1
"
}
);
Ok(())
}

View File

@@ -0,0 +1,737 @@
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn zero_value_handling() -> anyhow::Result<()> {
let result = compile! {
check "
let x = 0;
let y = x + 0;
let z = x * 100;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
result.output,
indoc! {
"
j main
main:
move r8 0
add r1 r8 0
move r9 r1
mul r2 r8 100
move r10 r2
"
}
);
Ok(())
}
#[test]
fn negative_number_handling() -> anyhow::Result<()> {
let result = compile! {
check "
let x = -100;
let y = -x;
let z = -(-50);
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
result.output,
indoc! {
"
j main
main:
move r8 -100
sub r1 0 r8
move r9 r1
move r10 50
"
}
);
Ok(())
}
#[test]
fn large_number_constants() -> anyhow::Result<()> {
let result = compile! {
check "
let x = 999999999;
let y = x + 1;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
result.output,
indoc! {
"
j main
main:
move r8 999999999
add r1 r8 1
move r9 r1
"
}
);
Ok(())
}
#[test]
fn floating_point_precision() -> anyhow::Result<()> {
let result = compile! {
check "
let pi = 3.14159265;
let e = 2.71828182;
let sum = pi + e;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
result.output,
indoc! {
"
j main
main:
move r8 3.14159265
move r9 2.71828182
add r1 r8 r9
move r10 r1
"
}
);
Ok(())
}
#[test]
fn temperature_unit_conversion() -> anyhow::Result<()> {
let result = compile! {
check "
let celsius = 20c;
let fahrenheit = 68f;
let kelvin = 293.15k;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
result.output,
indoc! {
"
j main
main:
move r8 293.15
move r9 293.15
move r10 293.15
"
}
);
Ok(())
}
#[test]
fn mixed_temperature_units() -> anyhow::Result<()> {
let compiled = compile! {
check "
let c = 0c;
let f = 32f;
let k = 273.15k;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 273.15
move r9 273.15
move r10 273.15
"
}
);
Ok(())
}
#[test]
fn boolean_constant_folding() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = true;
let y = false;
let z = true && true;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 1
move r9 0
and r1 1 1
move r10 r1
"
}
);
Ok(())
}
#[test]
fn empty_block() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 5;
{
}
let y = x;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 5
move r9 r8
"
}
);
Ok(())
}
#[test]
fn multiple_statements_same_line() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 1; let y = 2; let z = 3;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 1
move r9 2
move r10 3
"
}
);
Ok(())
}
#[test]
fn function_with_no_return() -> anyhow::Result<()> {
let compiled = compile! {
check "
fn no_return() {
let x = 5;
};
no_return();
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
no_return:
push sp
push ra
move r8 5
__internal_L1:
pop ra
pop sp
j ra
main:
jal no_return
move r1 r15
"
}
);
Ok(())
}
#[test]
fn deeply_nested_expressions() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = ((((((((1 + 2) + 3) + 4) + 5) + 6) + 7) + 8) + 9);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 45
"
}
);
Ok(())
}
#[test]
fn constant_folding_with_operations() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 10 * 5 + 3 - 2;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 51
"
}
);
Ok(())
}
#[test]
fn constant_folding_with_division() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 100 / 2 / 5;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 10
"
}
);
Ok(())
}
#[test]
fn modulo_operation() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 17 % 5;
let y = 10 % 3;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 2
move r9 1
"
}
);
Ok(())
}
#[test]
fn exponentiation() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 2 ** 8;
let y = 3 ** 3;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
pow r1 2 8
move r8 r1
pow r2 3 3
move r9 r2
"
}
);
Ok(())
}
#[test]
fn comparison_with_zero() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 0 == 0;
let y = 0 < 1;
let z = 0 > -1;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
seq r1 0 0
move r8 r1
slt r2 0 1
move r9 r2
sgt r3 0 -1
move r10 r3
"
}
);
Ok(())
}
#[test]
fn boolean_negation_edge_cases() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = !0;
let y = !1;
let z = !100;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
seq r1 0 0
move r8 r1
seq r2 1 0
move r9 r2
seq r3 100 0
move r10 r3
"
}
);
Ok(())
}
#[test]
fn function_with_many_parameters() -> anyhow::Result<()> {
let compiled = compile! {
check "
fn many_params(a, b, c, d, e, f, g, h) {
return a + b + c + d + e + f + g + h;
};
let result = many_params(1, 2, 3, 4, 5, 6, 7, 8);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
many_params:
pop r8
pop r9
pop r10
pop r11
pop r12
pop r13
pop r14
push sp
push ra
sub r0 sp 3
get r1 db r0
add r2 r1 r14
add r3 r2 r13
add r4 r3 r12
add r5 r4 r11
add r6 r5 r10
add r7 r6 r9
add r1 r7 r8
move r15 r1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
push 1
push 2
push 3
push 4
push 5
push 6
push 7
push 8
jal many_params
move r8 r15
"
}
);
Ok(())
}
#[test]
fn tuple_declaration_with_functions() -> anyhow::Result<()> {
let compiled = compile! {
check
r#"
device self = "db";
fn doSomething() {
return (self.Setting, self.Temperature);
}
let (setting, temperature) = doSomething();
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {"
j main
doSomething:
push sp
push ra
l r1 db Setting
push r1
l r2 db Temperature
push r2
sub r0 sp 4
get r0 db r0
move r15 r0
j __internal_L1
__internal_L1:
sub r0 sp 3
get ra db r0
j ra
main:
jal doSomething
pop r9
pop r8
move sp r15
"}
);
Ok(())
}
#[test]
fn tuple_from_simple_function() -> anyhow::Result<()> {
let compiled = compile! {
check "
fn get_pair() {
return (1, 2);
}
let (a, b) = get_pair();
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {"
j main
get_pair:
push sp
push ra
push 1
push 2
sub r0 sp 4
get r0 db r0
move r15 r0
j __internal_L1
__internal_L1:
sub r0 sp 3
get ra db r0
j ra
main:
jal get_pair
pop r9
pop r8
move sp r15
"}
);
Ok(())
}
#[test]
fn tuple_from_expression_not_function() -> anyhow::Result<()> {
let compiled = compile! {
check "
let (a, b) = (5 + 3, 10 * 2);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {"
j main
main:
move r8 8
move r9 20
"}
);
Ok(())
}

View File

@@ -0,0 +1,197 @@
use crate::Error;
use crate::variable_manager::Error as ScopeError;
#[test]
fn unknown_identifier_error() {
let errors = compile! {
result "let x = unknown_var;"
};
assert_eq!(errors.len(), 1);
match &errors[0] {
Error::UnknownIdentifier(name, _) => {
assert_eq!(name.as_ref(), "unknown_var");
}
_ => panic!("Expected UnknownIdentifier error, got {:?}", errors[0]),
}
}
#[test]
fn duplicate_identifier_error() {
let errors = compile! {
result "
let x = 5;
let x = 10;
"
};
assert_eq!(errors.len(), 1);
match &errors[0] {
Error::Scope(ScopeError::DuplicateVariable(name, _)) => {
assert_eq!(name.as_ref(), "x");
}
_ => panic!("Expected DuplicateIdentifier error, got {:?}", errors[0]),
}
}
#[test]
fn const_reassignment_error() {
let errors = compile! {
result "
const PI = 3.14;
PI = 2.71;
"
};
assert_eq!(errors.len(), 1);
match &errors[0] {
Error::ConstAssignment(name, _) => {
assert_eq!(name.as_ref(), "PI");
}
_ => panic!("Expected ConstAssignment error, got {:?}", errors[0]),
}
}
#[test]
fn unknown_function_call_error() {
let errors = compile! {
result "
let result = unknown_function();
"
};
assert_eq!(errors.len(), 1);
match &errors[0] {
Error::UnknownIdentifier(name, _) => {
assert_eq!(name.as_ref(), "unknown_function");
}
_ => panic!("Expected UnknownIdentifier error, got {:?}", errors[0]),
}
}
#[test]
fn argument_mismatch_error() {
let errors = compile! {
result "
fn add(a, b) {
return a + b;
};
let result = add(1);
"
};
// The error should be an AgrumentMismatch
assert!(
errors
.iter()
.any(|e| matches!(e, Error::AgrumentMismatch(_, _)))
);
}
#[test]
fn tuple_size_mismatch_error() {
let errors = compile! {
result "
fn pair() {
return (1, 2);
};
let (x, y, z) = pair();
"
};
assert!(
errors
.iter()
.any(|e| matches!(e, Error::TupleSizeMismatch(2, 3, _)))
);
}
#[test]
fn multiple_errors_reported() {
let errors = compile! {
result "
let x = unknown1;
let x = 5;
let y = unknown2;
"
};
// Should have at least 3 errors
assert!(
errors.len() >= 2,
"Expected at least 2 errors, got {}",
errors.len()
);
}
#[test]
fn return_outside_function_error() {
let errors = compile! {
result "
let x = 5;
return x;
"
};
// Should have an error about return outside function
assert!(
!errors.is_empty(),
"Expected error for return outside function"
);
}
#[test]
fn break_outside_loop_error() {
let errors = compile! {
result "
break;
"
};
assert!(!errors.is_empty(), "Expected error for break outside loop");
}
#[test]
fn continue_outside_loop_error() {
let errors = compile! {
result "
continue;
"
};
assert!(
!errors.is_empty(),
"Expected error for continue outside loop"
);
}
#[test]
fn device_reassignment_error() {
let errors = compile! {
result "
device d0 = \"d0\";
device d0 = \"d1\";
"
};
assert!(
errors
.iter()
.any(|e| matches!(e, Error::DuplicateIdentifier(_, _)))
);
}
#[test]
fn invalid_device_error() {
let errors = compile! {
result "
device d0 = \"d0\";
d0 = \"d1\";
"
};
// Device reassignment should fail
assert!(!errors.is_empty(), "Expected error for device reassignment");
}

View File

@@ -3,53 +3,149 @@ use pretty_assertions::assert_eq;
#[test]
fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
let compiled = compile!(debug r#"
let compiled = compile!(check r#"
// we need more than 4 params to 'spill' into a stack var
fn doSomething(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {};
fn doSomething(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {
return arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8 + arg9;
};
let item1 = 1;
let returned = doSomething(item1, 2, 3, 4, 5, 6, 7, 8, 9);
"#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {"
j main
doSomething:
pop r8 #arg9
pop r9 #arg8
pop r10 #arg7
pop r11 #arg6
pop r12 #arg5
pop r13 #arg4
pop r14 #arg3
pop r8
pop r9
pop r10
pop r11
pop r12
pop r13
pop r14
push sp
push ra
sub r0 sp 1
get ra db r0
sub sp sp 3
sub r0 sp 4
get r1 db r0
sub r0 sp 3
get r2 db r0
add r3 r1 r2
add r4 r3 r14
add r5 r4 r13
add r6 r5 r12
add r7 r6 r11
add r1 r7 r10
add r2 r1 r9
add r3 r2 r8
move r15 r3
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
move r8 1
push r8
push r8
push 2
push 3
push 4
push 5
push 6
push 7
push 8
push 9
jal doSomething
pop r8
move r9 r15
"}
);
Ok(())
}
#[test]
fn test_early_return() -> anyhow::Result<()> {
let compiled = compile!(check r#"
// This is a test function declaration with no body
fn doSomething() {
if (1 == 1) {
return;
}
let i = 1 + 2;
return;
};
doSomething();
"#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
doSomething:
push sp
push ra
seq r1 1 1
beqz r1 __internal_L2
j __internal_L1
__internal_L2:
move r8 3
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
jal doSomething
move r1 r15
"
}
);
Ok(())
}
#[test]
fn test_function_declaration_with_register_params() -> anyhow::Result<()> {
let compiled = compile!(debug r#"
let compiled = compile!(check r#"
// This is a test function declaration with no body
fn doSomething(arg1, arg2) {
};
"#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {"
j main
doSomething:
pop r8 #arg2
pop r9 #arg1
pop r8
pop r9
push sp
push ra
sub r0 sp 1
get ra db r0
sub sp sp 1
__internal_L1:
pop ra
pop sp
j ra
"}
);

View File

@@ -1,11 +1,10 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_comparison_expressions() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let isGreater = 10 > 5;
let isLess = 5 < 10;
@@ -16,24 +15,30 @@ fn test_comparison_expressions() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
sgt r1 10 5
move r8 r1 #isGreater
move r8 r1
slt r2 5 10
move r9 r2 #isLess
move r9 r2
seq r3 5 5
move r10 r3 #isEqual
move r10 r3
sne r4 5 10
move r11 r4 #isNotEqual
move r11 r4
sge r5 10 10
move r12 r5 #isGreaterOrEqual
move r12 r5
sle r6 5 5
move r13 r6 #isLessOrEqual
move r13 r6
"
}
);
@@ -43,8 +48,8 @@ fn test_comparison_expressions() -> anyhow::Result<()> {
#[test]
fn test_logical_and_or_not() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let logic1 = 1 && 1;
let logic2 = 1 || 0;
@@ -52,18 +57,24 @@ fn test_logical_and_or_not() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
and r1 1 1
move r8 r1 #logic1
move r8 r1
or r2 1 0
move r9 r2 #logic2
move r9 r2
seq r3 1 0
move r10 r3 #logic3
move r10 r3
"
}
);
@@ -73,15 +84,21 @@ fn test_logical_and_or_not() -> anyhow::Result<()> {
#[test]
fn test_complex_logic() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let logic = (10 > 5) && (5 < 10);
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -89,7 +106,7 @@ fn test_complex_logic() -> anyhow::Result<()> {
sgt r1 10 5
slt r2 5 10
and r3 r1 r2
move r8 r3 #logic
move r8 r3
"
}
);
@@ -99,21 +116,27 @@ fn test_complex_logic() -> anyhow::Result<()> {
#[test]
fn test_math_with_logic() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let logic = (1 + 2) > 1;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
sgt r1 3 1
move r8 r1 #logic
move r8 r1
"
}
);
@@ -123,21 +146,27 @@ fn test_math_with_logic() -> anyhow::Result<()> {
#[test]
fn test_boolean_in_logic() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let res = true && false;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
and r1 1 0
move r8 r1 #res
move r8 r1
"
}
);
@@ -147,8 +176,8 @@ fn test_boolean_in_logic() -> anyhow::Result<()> {
#[test]
fn test_invert_a_boolean() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let i = true;
let y = !i;
@@ -157,17 +186,23 @@ fn test_invert_a_boolean() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 1 #i
move r8 1
seq r1 r8 0
move r9 r1 #y
move r9 r1
seq r2 r9 0
move r10 r2 #result
move r10 r2
"
}
);

View File

@@ -1,11 +1,10 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_infinite_loop() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
loop {
@@ -14,19 +13,25 @@ fn test_infinite_loop() -> anyhow::Result<()> {
"
};
// Labels: L1 (start), L2 (end)
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
// __internal_Labels: L1 (start), L2 (end)
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 0 #a
L1:
move r8 0
__internal_L1:
add r1 r8 1
move r8 r1 #a
j L1
L2:
move r8 r1
j __internal_L1
__internal_L2:
"
}
);
@@ -36,8 +41,8 @@ fn test_infinite_loop() -> anyhow::Result<()> {
#[test]
fn test_loop_break() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
loop {
@@ -49,23 +54,29 @@ fn test_loop_break() -> anyhow::Result<()> {
"
};
// Labels: L1 (start), L2 (end), L3 (if end - implicit else label)
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
// __internal_Labels: L1 (start), L2 (end), L3 (if end - implicit else label)
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 0 #a
L1:
move r8 0
__internal_L1:
add r1 r8 1
move r8 r1 #a
move r8 r1
sgt r2 r8 10
beq r2 0 L3
j L2
L3:
j L1
L2:
beqz r2 __internal_L3
j __internal_L2
__internal_L3:
j __internal_L1
__internal_L2:
"
}
);
@@ -75,8 +86,8 @@ fn test_loop_break() -> anyhow::Result<()> {
#[test]
fn test_while_loop() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
while (a < 10) {
@@ -85,21 +96,27 @@ fn test_while_loop() -> anyhow::Result<()> {
"
};
// Labels: L1 (start), L2 (end)
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
// __internal_Labels: L1 (start), L2 (end)
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 0 #a
L1:
move r8 0
__internal_L1:
slt r1 r8 10
beq r1 0 L2
beqz r1 __internal_L2
add r2 r8 1
move r8 r2 #a
j L1
L2:
move r8 r2
j __internal_L1
__internal_L2:
"
}
);
@@ -109,8 +126,8 @@ fn test_while_loop() -> anyhow::Result<()> {
#[test]
fn test_loop_continue() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
r#"
let a = 0;
loop {
@@ -123,24 +140,30 @@ fn test_loop_continue() -> anyhow::Result<()> {
"#
};
// Labels: L1 (start), L2 (end), L3 (if end)
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
// __internal_Labels: L1 (start), L2 (end), L3 (if end)
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
main:
move r8 0 #a
L1:
move r8 0
__internal_L1:
add r1 r8 1
move r8 r1 #a
move r8 r1
slt r2 r8 5
beq r2 0 L3
j L1
L3:
j L2
j L1
L2:
beqz r2 __internal_L3
j __internal_L1
__internal_L3:
j __internal_L2
j __internal_L1
__internal_L2:
"
}
);

View File

@@ -1,4 +1,3 @@
use crate::compile;
use anyhow::Result;
use indoc::indoc;
use pretty_assertions::assert_eq;
@@ -6,20 +5,26 @@ use pretty_assertions::assert_eq;
#[test]
fn test_acos() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = acos(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
acos r15 123
move r8 r15 #i
move r8 r15
"
}
);
@@ -30,20 +35,26 @@ fn test_acos() -> Result<()> {
#[test]
fn test_asin() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = asin(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
asin r15 123
move r8 r15 #i
move r8 r15
"
}
);
@@ -54,20 +65,26 @@ fn test_asin() -> Result<()> {
#[test]
fn test_atan() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = atan(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
atan r15 123
move r8 r15 #i
move r8 r15
"
}
);
@@ -78,20 +95,26 @@ fn test_atan() -> Result<()> {
#[test]
fn test_atan2() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = atan2(123, 456);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
atan2 r15 123 456
move r8 r15 #i
move r8 r15
"
}
);
@@ -102,20 +125,26 @@ fn test_atan2() -> Result<()> {
#[test]
fn test_abs() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = abs(-123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
abs r15 -123
move r8 r15 #i
move r8 r15
"
}
);
@@ -126,20 +155,26 @@ fn test_abs() -> Result<()> {
#[test]
fn test_ceil() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = ceil(123.90);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
ceil r15 123.90
move r8 r15 #i
move r8 r15
"
}
);
@@ -150,20 +185,26 @@ fn test_ceil() -> Result<()> {
#[test]
fn test_cos() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = cos(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
cos r15 123
move r8 r15 #i
move r8 r15
"
}
);
@@ -174,20 +215,26 @@ fn test_cos() -> Result<()> {
#[test]
fn test_floor() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = floor(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
floor r15 123
move r8 r15 #i
move r8 r15
"
}
);
@@ -198,20 +245,26 @@ fn test_floor() -> Result<()> {
#[test]
fn test_log() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = log(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
log r15 123
move r8 r15 #i
move r8 r15
"
}
);
@@ -222,20 +275,26 @@ fn test_log() -> Result<()> {
#[test]
fn test_max() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = max(123, 456);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
max r15 123 456
move r8 r15 #i
move r8 r15
"
}
);
@@ -246,22 +305,28 @@ fn test_max() -> Result<()> {
#[test]
fn test_max_from_game() -> Result<()> {
let compiled = compile! {
debug
check
r#"
let item = 0;
item = max(1 + 2, 2);
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
move r8 0 #item
move r8 0
max r15 3 2
move r8 r15 #item
move r8 r15
"
}
);
@@ -272,20 +337,26 @@ fn test_max_from_game() -> Result<()> {
#[test]
fn test_min() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = min(123, 456);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
min r15 123 456
move r8 r15 #i
move r8 r15
"
}
);
@@ -296,20 +367,26 @@ fn test_min() -> Result<()> {
#[test]
fn test_rand() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = rand();
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
rand r15
move r8 r15 #i
move r8 r15
"
}
);
@@ -320,20 +397,26 @@ fn test_rand() -> Result<()> {
#[test]
fn test_sin() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = sin(3);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
sin r15 3
move r8 r15 #i
move r8 r15
"
}
);
@@ -344,20 +427,26 @@ fn test_sin() -> Result<()> {
#[test]
fn test_sqrt() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = sqrt(3);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
sqrt r15 3
move r8 r15 #i
move r8 r15
"
}
);
@@ -368,20 +457,26 @@ fn test_sqrt() -> Result<()> {
#[test]
fn test_tan() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = tan(3);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
tan r15 3
move r8 r15 #i
move r8 r15
"
}
);
@@ -392,20 +487,26 @@ fn test_tan() -> Result<()> {
#[test]
fn test_trunc() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = trunc(3.234);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
trunc r15 3.234
move r8 r15 #i
move r8 r15
"
}
);

View File

@@ -6,46 +6,60 @@ macro_rules! output {
};
}
/// Represents both compilation errors and compiled output
pub struct CompilationCheckResult {
pub errors: Vec<crate::Error<'static>>,
pub output: String,
}
#[cfg_attr(test, macro_export)]
macro_rules! compile {
($source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = ::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))),
&mut writer,
None,
);
compiler.compile();
let res = compiler.compile();
res.instructions.write(&mut writer)?;
output!(writer)
}};
(result $source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from($source)),
&mut writer,
Some(crate::CompilerConfig { debug: true }),
);
compiler.compile()
compiler.compile().errors
}};
(debug $source:expr) => {{
(check $source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from($source)),
&mut writer,
Some(crate::CompilerConfig { debug: true }),
);
compiler.compile();
output!(writer)
let res = compiler.compile();
res.instructions.write(&mut writer)?;
let output = output!(writer);
crate::test::CompilationCheckResult {
errors: res.errors,
output,
}
}};
}
mod binary_expression;
mod branching;
mod declaration_function_invocation;
mod declaration_literal;
mod device_access;
mod edge_cases;
mod error_handling;
mod function_declaration;
mod logic_expression;
mod loops;
mod math_syscall;
mod negation_priority;
mod scoping;
mod syscall;
mod tuple_literals;

View File

@@ -0,0 +1,388 @@
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn simple_negation() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = -5;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 -5
"
}
);
Ok(())
}
#[test]
fn negation_of_variable() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 10;
let y = -x;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 10
sub r1 0 r8
move r9 r1
"
}
);
Ok(())
}
#[test]
fn double_negation() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = -(-5);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 5
"
}
);
Ok(())
}
#[test]
fn negation_in_expression() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 10 + (-5);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 5
"
}
);
Ok(())
}
#[test]
fn negation_with_multiplication() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = -3 * 4;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 -12
"
}
);
Ok(())
}
#[test]
fn parentheses_priority() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = (2 + 3) * 4;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 20
"
}
);
Ok(())
}
#[test]
fn nested_parentheses() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = ((2 + 3) * (4 - 1));
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 15
"
}
);
Ok(())
}
#[test]
fn parentheses_with_variables() -> anyhow::Result<()> {
let compiled = compile! {
check "
let a = 5;
let b = 10;
let c = (a + b) * 2;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
// Should calculate (5 + 10) * 2 = 30
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 5
move r9 10
add r1 r8 r9
mul r2 r1 2
move r10 r2
"
}
);
Ok(())
}
#[test]
fn priority_affects_result() -> anyhow::Result<()> {
let compiled = compile! {
check "
let with_priority = (2 + 3) * 4;
let without_priority = 2 + 3 * 4;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
// with_priority should be 20, without_priority should be 14
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 20
move r9 14
"
}
);
Ok(())
}
#[test]
fn negation_of_expression() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = -(2 + 3);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
// Should be -5 (constant folded)
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
sub r1 0 5
move r8 r1
"
}
);
Ok(())
}
#[test]
fn complex_negation_and_priority() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = -((10 - 5) * 2);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
// Should be -(5 * 2) = -10 (folded to constant)
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
sub r1 0 10
move r8 r1
"
}
);
Ok(())
}
#[test]
fn negation_in_logical_expression() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = !(-5);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
// -5 is truthy, so !(-5) should be 0
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
sub r1 0 5
seq r2 r1 0
move r8 r2
"
}
);
Ok(())
}
#[test]
fn parentheses_in_comparison() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = (10 + 5) > (3 * 4);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
// (10 + 5) = 15 > (3 * 4) = 12, so true (1)
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
sgt r1 15 12
move r8 r1
"
}
);
Ok(())
}

View File

@@ -0,0 +1,462 @@
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn block_scope() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 10;
{
let y = 20;
let z = x + y;
}
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 10
move r9 20
add r1 r8 r9
move r10 r1
"
}
);
Ok(())
}
#[test]
fn variable_scope_isolation() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 10;
{
let x = 20;
let y = x;
}
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 10
move r9 20
move r10 r9
"
}
);
Ok(())
}
#[test]
fn function_parameter_scope() -> anyhow::Result<()> {
let compiled = compile! {
check "
fn double(x) {
return x * 2;
};
let result = double(5);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
double:
pop r8
push sp
push ra
mul r1 r8 2
move r15 r1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
push 5
jal double
move r8 r15
"
}
);
Ok(())
}
#[test]
fn nested_block_scopes() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 1;
{
let x = 2;
{
let x = 3;
let y = x;
}
let z = x;
}
let w = x;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 1
move r9 2
move r10 3
move r11 r10
move r10 r9
move r9 r8
"
}
);
Ok(())
}
#[test]
fn variable_shadowing_in_conditional() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 10;
if (true) {
let x = 20;
}
let y = x;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 10
beqz 1 __internal_L1
move r9 20
__internal_L1:
move r9 r8
"
}
);
Ok(())
}
#[test]
fn variable_shadowing_in_loop() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 0;
loop {
let x = x + 1;
if (x > 5) {
break;
}
}
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 0
__internal_L1:
add r1 r8 1
move r9 r1
sgt r2 r9 5
beqz r2 __internal_L3
j __internal_L2
__internal_L3:
j __internal_L1
__internal_L2:
"
}
);
Ok(())
}
#[test]
fn const_scope() -> anyhow::Result<()> {
let compiled = compile! {
check "
const PI = 3.14;
{
const PI = 2.71;
let x = PI;
}
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 2.71
"
}
);
Ok(())
}
#[test]
fn device_in_scope() -> anyhow::Result<()> {
let compiled = compile! {
check "
device d0 = \"d0\";
{
let value = d0.Temperature;
}
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
l r1 d0 Temperature
move r8 r1
"
}
);
Ok(())
}
#[test]
fn function_scope_isolation() -> anyhow::Result<()> {
let compiled = compile! {
check "
fn func1() {
let x = 10;
return x;
};
fn func2() {
let x = 20;
return x;
};
let a = func1();
let b = func2();
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
func1:
push sp
push ra
move r8 10
move r15 r8
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
func2:
push sp
push ra
move r8 20
move r15 r8
j __internal_L2
__internal_L2:
pop ra
pop sp
j ra
main:
jal func1
move r8 r15
push r8
jal func2
pop r8
move r9 r15
"
}
);
Ok(())
}
#[test]
fn tuple_unpacking_scope() -> anyhow::Result<()> {
let compiled = compile! {
check "
fn pair() {
return (1, 2);
};
{
let (x, y) = pair();
let z = x + y;
}
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
pair:
push sp
push ra
push 1
push 2
sub r0 sp 4
get r0 db r0
move r15 r0
j __internal_L1
__internal_L1:
sub r0 sp 3
get ra db r0
j ra
main:
jal pair
pop r9
pop r8
move sp r15
add r1 r8 r9
move r10 r1
"
}
);
Ok(())
}
#[test]
fn shadowing_doesnt_affect_outer() -> anyhow::Result<()> {
let compiled = compile! {
check "
let x = 5;
let y = x;
{
let x = 10;
let z = x;
}
let w = x + y;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 5
move r9 r8
move r10 10
move r11 r10
add r1 r8 r9
move r10 r1
"
}
);
Ok(())
}

View File

@@ -1,18 +1,23 @@
use crate::compile;
use indoc::indoc;
use pretty_assertions::assert_eq;
#[test]
fn test_yield() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
yield();
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -28,7 +33,7 @@ fn test_yield() -> anyhow::Result<()> {
#[test]
fn test_sleep() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
sleep(3);
let sleepAmount = 15;
@@ -37,14 +42,20 @@ fn test_sleep() -> anyhow::Result<()> {
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
sleep 3
move r8 15 #sleepAmount
move r8 15
sleep r8
mul r1 r8 2
sleep r1
@@ -58,7 +69,7 @@ fn test_sleep() -> anyhow::Result<()> {
#[test]
fn test_set_on_device() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device airConditioner = "d0";
let internalTemp = 20c;
@@ -67,13 +78,19 @@ fn test_set_on_device() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
move r8 293.15 #internalTemp
move r8 293.15
sgt r1 r8 298.15
s d0 On r1
"
@@ -86,15 +103,21 @@ fn test_set_on_device() -> anyhow::Result<()> {
#[test]
fn test_set_on_device_batched() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
const doorHash = hash("Door");
setBatched(doorHash, "Lock", true);
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
r#"
j main
@@ -109,7 +132,7 @@ fn test_set_on_device_batched() -> anyhow::Result<()> {
#[test]
fn test_set_on_device_batched_named() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device dev = "d0";
const devName = hash("test");
@@ -118,8 +141,14 @@ fn test_set_on_device_batched_named() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -135,7 +164,7 @@ fn test_set_on_device_batched_named() -> anyhow::Result<()> {
#[test]
fn test_load_from_device() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device airCon = "d0";
@@ -143,14 +172,20 @@ fn test_load_from_device() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
l r15 d0 On
move r8 r15 #setting
move r8 r15
"
}
);
@@ -161,7 +196,7 @@ fn test_load_from_device() -> anyhow::Result<()> {
#[test]
fn test_load_from_slot() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device airCon = "d0";
@@ -169,14 +204,20 @@ fn test_load_from_slot() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
main:
ls r15 d0 0 Occupied
move r8 r15 #setting
move r8 r15
"
}
);
@@ -187,7 +228,7 @@ fn test_load_from_slot() -> anyhow::Result<()> {
#[test]
fn test_set_slot() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device airCon = "d0";
@@ -195,8 +236,14 @@ fn test_set_slot() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -208,3 +255,35 @@ fn test_set_slot() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn test_load_reagent() -> anyhow::Result<()> {
let compiled = compile! {
check
r#"
device thingy = "d0";
let something = lr(thingy, "Contents", hash("Iron"));
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
lr r15 d0 Contents -666742878
move r8 r15
"
}
);
Ok(())
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,9 @@
// r1 - r7 : Temporary Variables
// r8 - r14 : Persistant Variables
use helpers::Span;
use lsp_types::{Diagnostic, DiagnosticSeverity};
use parser::tree_node::{Literal, Span};
use parser::tree_node::Literal;
use std::{
borrow::Cow,
collections::{HashMap, VecDeque},
@@ -94,19 +95,21 @@ impl<'a, 'b> VariableScope<'a, 'b> {
pub const RETURN_REGISTER: u8 = 15;
pub const TEMP_STACK_REGISTER: u8 = 0;
pub fn registers(&self) -> impl Iterator<Item = &u8> {
self.var_lookup_table
.values()
.filter(|val| {
matches!(
val,
VariableLocation::Temporary(_) | VariableLocation::Persistant(_)
)
})
.map(|loc| match loc {
VariableLocation::Persistant(reg) | VariableLocation::Temporary(reg) => reg,
_ => unreachable!(),
})
pub fn registers(&self) -> Vec<u8> {
let mut used = Vec::new();
for r in TEMP {
if !self.temporary_vars.contains(&r) {
used.push(r);
}
}
for r in PERSIST {
if !self.persistant_vars.contains(&r) {
used.push(r);
}
}
used
}
pub fn scoped(parent: &'b VariableScope<'a, 'b>) -> Self {

View File

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

View File

@@ -2,6 +2,44 @@ mod helper_funcs;
mod macros;
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.
/// You can easily create documentation for large enums with the `documented!` macro.
pub trait Documentation {

View File

@@ -10,6 +10,7 @@ macro_rules! with_syscalls {
"loadBatched",
"loadBatchedNamed",
"loadSlot",
"loadReagent",
"set",
"setBatched",
"setBatchedNamed",
@@ -35,6 +36,7 @@ macro_rules! with_syscalls {
"lb",
"lbn",
"ls",
"lr",
"s",
"sb",
"sbn",

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,356 @@
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>),
/// `lr register device reagentMode int`
LoadReagent(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::LoadReagent(reg, device, reagent_mode, reagent_hash) => {
write!(f, "lr {} {} {} {}", reg, device, reagent_mode, reagent_hash)
}
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,48 @@
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 label.starts_with("__internal_L") {
continue;
}
// 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,882 @@
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) if !label.starts_with("__internal_L") => {
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
&& !l.starts_with("__internal_L")
{
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::LoadReagent(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::LoadReagent(_, b, c, d) => {
Some(Instruction::LoadReagent(r, 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::LoadReagent(_, device, _, item_hash) => check(device) || check(item_hash),
Instruction::LoadSlot(_, dev, slot, _) => check(dev) || check(slot),
Instruction::LoadBatch(_, dev, _, mode) => check(dev) || check(mode),
Instruction::LoadBatchNamed(_, d_hash, n_hash, _, mode) => {
check(d_hash) || check(n_hash) || check(mode)
}
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

@@ -7,6 +7,7 @@ edition = "2024"
tokenizer = { path = "../tokenizer" }
helpers = { path = "../helpers" }
lsp-types = { workspace = true }
safer-ffi = { workspace = true }
thiserror = { workspace = true }

View File

@@ -4,6 +4,7 @@ mod test;
pub mod tree_node;
use crate::sys_call::{Math, System};
use helpers::Span;
use std::{borrow::Cow, io::SeekFrom};
use sys_call::SysCall;
use thiserror::Error;
@@ -293,12 +294,12 @@ impl<'a> Parser<'a> {
// Handle Infix operators (Binary, Logical, Assignment)
if self_matches_peek!(
self,
TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign)
TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign | Symbol::Question)
) {
return Ok(Some(self.infix(lhs)?));
} else if self_matches_current!(
self,
TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign)
TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign | Symbol::Question)
) {
self.tokenizer.seek(SeekFrom::Current(-1))?;
return Ok(Some(self.infix(lhs)?));
@@ -440,7 +441,13 @@ impl<'a> Parser<'a> {
));
}
TokenType::Keyword(Keyword::Let) => Some(self.spanned(|p| p.declaration())?),
TokenType::Keyword(Keyword::Let) => {
if self_matches_peek!(self, TokenType::Symbol(Symbol::LParen)) {
Some(self.spanned(|p| p.tuple_declaration())?)
} else {
Some(self.spanned(|p| p.declaration())?)
}
}
TokenType::Keyword(Keyword::Device) => {
let spanned_dev = self.spanned(|p| p.device())?;
@@ -560,9 +567,7 @@ impl<'a> Parser<'a> {
})
}
TokenType::Symbol(Symbol::LParen) => {
self.spanned(|p| p.priority())?.node.map(|node| *node)
}
TokenType::Symbol(Symbol::LParen) => self.parenthesized_or_tuple()?,
TokenType::Symbol(Symbol::Minus) => {
let start_span = self.current_span();
@@ -641,8 +646,8 @@ impl<'a> Parser<'a> {
}
}
TokenType::Symbol(Symbol::LParen) => *self
.spanned(|p| p.priority())?
.node
.parenthesized_or_tuple()?
.map(Box::new)
.ok_or(Error::UnexpectedEOF)?,
TokenType::Identifier(ref id) if SysCall::is_syscall(id) => {
@@ -766,12 +771,15 @@ impl<'a> Parser<'a> {
Expression::Binary(_)
| Expression::Logical(_)
| Expression::Invocation(_)
| Expression::Syscall(_)
| Expression::Priority(_)
| Expression::Literal(_)
| Expression::Variable(_)
| Expression::Ternary(_)
| Expression::Negation(_)
| Expression::MemberAccess(_)
| Expression::MethodCall(_) => {}
| Expression::MethodCall(_)
| Expression::Tuple(_) => {}
_ => {
return Err(Error::InvalidSyntax(
self.current_span(),
@@ -788,7 +796,7 @@ impl<'a> Parser<'a> {
// Include Assign in the operator loop
while token_matches!(
temp_token,
TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign)
TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign | Symbol::Question | Symbol::Colon)
) {
let operator = match temp_token.token_type {
TokenType::Symbol(s) => s,
@@ -1019,7 +1027,52 @@ impl<'a> Parser<'a> {
}
operators.retain(|symbol| !matches!(symbol, Symbol::LogicalOr));
// --- PRECEDENCE LEVEL 8: Assignment (=) ---
// -- PRECEDENCE LEVEL 8: Ternary (x ? 1 : 2)
for i in (0..operators.len()).rev() {
if matches!(operators[i], Symbol::Question) {
// Ensure next operator is a colon
if i + 1 >= operators.len() || !matches!(operators[i + 1], Symbol::Colon) {
return Err(Error::InvalidSyntax(
self.current_span(),
"Ternary operator '?' missing matching ':'".to_string(),
));
}
let false_branch = expressions.remove(i + 2);
let true_branch = expressions.remove(i + 1);
let condition = expressions.remove(i);
let span = Span {
start_line: condition.span.start_line,
end_line: false_branch.span.end_line,
start_col: condition.span.start_col,
end_col: false_branch.span.end_col,
};
let ternary_node = Spanned {
span,
node: TernaryExpression {
condition: Box::new(condition),
true_value: Box::new(true_branch),
false_value: Box::new(false_branch),
},
};
expressions.insert(
i,
Spanned {
node: Expression::Ternary(ternary_node),
span,
},
);
// Remove the `?` and the `:` from the operators list
operators.remove(i);
operators.remove(i);
}
}
// --- PRECEDENCE LEVEL 9: Assignment (=) ---
// Assignment is Right Associative: a = b = c => a = (b = c)
// We iterate Right to Left
for (i, operator) in operators.iter().enumerate().rev() {
@@ -1033,19 +1086,39 @@ impl<'a> Parser<'a> {
end_col: right.span.end_col,
};
expressions.insert(
i,
Spanned {
// Check if the left side is a tuple, and if so, create a TupleAssignment
let node = if let Expression::Tuple(tuple_expr) = &left.node {
// Extract variable names from the tuple, handling underscores
let mut names = Vec::new();
for item in &tuple_expr.node {
if let Expression::Variable(var) = &item.node {
names.push(var.clone());
} else {
return Err(Error::InvalidSyntax(
item.span,
String::from("Tuple assignment can only contain variable names"),
));
}
}
Expression::TupleAssignment(Spanned {
span,
node: Expression::Assignment(Spanned {
span,
node: AssignmentExpression {
assignee: boxed!(left),
expression: boxed!(right),
},
}),
},
);
node: TupleAssignmentExpression {
names,
value: boxed!(right),
},
})
} else {
Expression::Assignment(Spanned {
span,
node: AssignmentExpression {
assignee: boxed!(left),
expression: boxed!(right),
},
})
};
expressions.insert(i, Spanned { span, node });
}
}
operators.retain(|symbol| !matches!(symbol, Symbol::Assign));
@@ -1069,8 +1142,12 @@ impl<'a> Parser<'a> {
expressions.pop().ok_or(Error::UnexpectedEOF)
}
fn priority(&mut self) -> Result<Option<Box<Spanned<Expression<'a>>>>, Error<'a>> {
fn parenthesized_or_tuple(
&mut self,
) -> Result<Option<Spanned<tree_node::Expression<'a>>>, Error<'a>> {
let start_span = self.current_span();
let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?;
if !token_matches!(current_token, TokenType::Symbol(Symbol::LParen)) {
return Err(Error::UnexpectedToken(
self.current_span(),
@@ -1079,17 +1156,112 @@ impl<'a> Parser<'a> {
}
self.assign_next()?;
let expression = self.expression()?.ok_or(Error::UnexpectedEOF)?;
let current_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(current_token, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken(
Self::token_to_span(&current_token),
current_token,
));
// Handle empty tuple '()'
if self_matches_peek!(self, TokenType::Symbol(Symbol::RParen)) {
self.assign_next()?;
let end_span = self.current_span();
let span = Span {
start_line: start_span.start_line,
start_col: start_span.start_col,
end_line: end_span.end_line,
end_col: end_span.end_col,
};
return Ok(Some(Spanned {
span,
node: Expression::Tuple(Spanned { span, node: vec![] }),
}));
}
Ok(Some(boxed!(expression)))
let first_expression = self.expression()?.ok_or(Error::UnexpectedEOF)?;
if self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) {
// It is a tuple
let mut items = vec![first_expression];
while self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) {
// Next toekn is a comma, we need to consume it and advance 1 more time.
self.assign_next()?;
self.assign_next()?;
items.push(self.expression()?.ok_or(Error::UnexpectedEOF)?);
}
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&next), next));
}
let end_span = Self::token_to_span(&next);
let span = Span {
start_line: start_span.start_line,
start_col: start_span.start_col,
end_line: end_span.end_line,
end_col: end_span.end_col,
};
Ok(Some(Spanned {
span,
node: Expression::Tuple(Spanned { span, node: items }),
}))
} else {
// It is just priority
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&next), next));
}
Ok(Some(Spanned {
span: first_expression.span,
node: Expression::Priority(boxed!(first_expression)),
}))
}
}
fn tuple_declaration(&mut self) -> Result<Expression<'a>, Error<'a>> {
// 'let' is consumed before this call
// expect '('
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::LParen)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&next), next));
}
let mut names = Vec::new();
while !self_matches_peek!(self, TokenType::Symbol(Symbol::RParen)) {
let token = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
let span = Self::token_to_span(&token);
if let TokenType::Identifier(id) = token.token_type {
names.push(Spanned { span, node: id });
} else {
return Err(Error::UnexpectedToken(span, token));
}
if self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) {
self.assign_next()?;
}
}
self.assign_next()?; // consume ')'
let assign = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(assign, TokenType::Symbol(Symbol::Assign)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&assign), assign));
}
self.assign_next()?; // Consume the `=`
let value = self.expression()?.ok_or(Error::UnexpectedEOF)?;
let semi = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(semi, TokenType::Symbol(Symbol::Semicolon)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&semi), semi));
}
Ok(Expression::TupleDeclaration(Spanned {
span: names.first().map(|n| n.span).unwrap_or(value.span),
node: TupleDeclarationExpression {
names,
value: boxed!(value),
},
}))
}
fn invocation(&mut self) -> Result<InvocationExpression<'a>, Error<'a>> {
@@ -1179,18 +1351,34 @@ impl<'a> Parser<'a> {
// Need to capture return span
let ret_start_span = Self::token_to_span(&current_token);
self.assign_next()?;
let expression = self.expression()?.ok_or(Error::UnexpectedEOF)?;
let expr = if token_matches!(
self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?,
TokenType::Symbol(Symbol::Semicolon)
) {
// rewind 1 token so we can check for the semicolon at the bottom of this function.
self.tokenizer.seek(SeekFrom::Current(-1))?;
None
} else {
Some(self.expression()?.ok_or(Error::UnexpectedEOF)?)
};
let ret_span = Span {
start_line: ret_start_span.start_line,
start_col: ret_start_span.start_col,
end_line: expression.span.end_line,
end_col: expression.span.end_col,
end_line: expr
.as_ref()
.map(|e| e.span.end_line)
.unwrap_or(ret_start_span.end_line),
end_col: expr
.as_ref()
.map(|e| e.span.end_col)
.unwrap_or(ret_start_span.end_col),
};
let return_expr = Spanned {
span: ret_span,
node: Expression::Return(boxed!(expression)),
node: Expression::Return(expr.map(Box::new)),
};
expressions.push(return_expr);
@@ -1770,20 +1958,8 @@ impl<'a> Parser<'a> {
let mut args = args!(3);
let next = args.next();
let dev_name = literal_or_variable!(next);
let next = args.next();
let slot_index = get_arg!(Literal, literal_or_variable!(next));
if !matches!(
slot_index,
Spanned {
node: Literal::Number(_),
..
},
) {
return Err(Error::InvalidSyntax(
slot_index.span,
"Expected a number".to_string(),
));
}
let slot_index = args.next().ok_or(Error::UnexpectedEOF)?;
let next = args.next();
let slot_logic = get_arg!(Literal, literal_or_variable!(next));
if !matches!(
@@ -1800,27 +1976,17 @@ impl<'a> Parser<'a> {
}
Ok(SysCall::System(System::LoadSlot(
dev_name, slot_index, slot_logic,
dev_name,
boxed!(slot_index),
slot_logic,
)))
}
"setSlot" | "ss" => {
let mut args = args!(4);
let next = args.next();
let dev_name = literal_or_variable!(next);
let next = args.next();
let slot_index = get_arg!(Literal, literal_or_variable!(next));
if !matches!(
slot_index,
Spanned {
node: Literal::Number(_),
..
}
) {
return Err(Error::InvalidSyntax(
slot_index.span,
"Expected a number".into(),
));
}
let slot_index = args.next().ok_or(Error::UnexpectedEOF)?;
let next = args.next();
let slot_logic = get_arg!(Literal, literal_or_variable!(next));
if !matches!(
@@ -1840,9 +2006,23 @@ impl<'a> Parser<'a> {
Ok(SysCall::System(System::SetSlot(
dev_name,
slot_index,
boxed!(slot_index),
slot_logic,
Box::new(expr),
boxed!(expr),
)))
}
"loadReagent" | "lr" => {
let mut args = args!(3);
let next = args.next();
let device = literal_or_variable!(next);
let next = args.next();
let reagent_mode = get_arg!(Literal, literal_or_variable!(next));
let reagent_hash = args.next().ok_or(Error::UnexpectedEOF)?;
Ok(SysCall::System(System::LoadReagent(
device,
reagent_mode,
Box::new(reagent_hash),
)))
}

View File

@@ -223,7 +223,7 @@ documented! {
/// `let isOccupied = ls(deviceHash, 2, "Occupied");`
LoadSlot(
Spanned<LiteralOrVariable<'a>>,
Spanned<Literal<'a>>,
Box<Spanned<Expression<'a>>>,
Spanned<Literal<'a>>
),
/// Stores a value of LogicType on a device by the index value
@@ -234,7 +234,19 @@ documented! {
/// `ss(deviceHash, 0, "Open", true);`
SetSlot(
Spanned<LiteralOrVariable<'a>>,
Box<Spanned<Expression<'a>>>,
Spanned<Literal<'a>>,
Box<Spanned<Expression<'a>>>
),
/// Loads reagent of device's ReagentMode where a hash of the reagent type to check for
///
/// ## IC10
/// `lr r? device(d?|r?|id) reagentMode int`
/// ## Slang
/// `let result = loadReagent(deviceHash, "ReagentMode", reagentHash);`
/// `let result = lr(deviceHash, "ReagentMode", reagentHash);`
LoadReagent(
Spanned<LiteralOrVariable<'a>>,
Spanned<Literal<'a>>,
Box<Spanned<Expression<'a>>>
)
@@ -261,6 +273,7 @@ impl<'a> std::fmt::Display for System<'a> {
}
System::LoadSlot(a, b, c) => write!(f, "loadSlot({}, {}, {})", a, b, c),
System::SetSlot(a, b, c, d) => write!(f, "setSlot({}, {}, {}, {})", a, b, c, d),
System::LoadReagent(a, b, c) => write!(f, "loadReagent({}, {}, {})", a, b, c),
}
}
}

View File

@@ -1,6 +1,6 @@
use tokenizer::Tokenizer;
use crate::Parser;
use pretty_assertions::assert_eq;
use tokenizer::Tokenizer;
#[test]
fn test_block() -> anyhow::Result<()> {

View File

@@ -54,10 +54,7 @@ fn test_const_declaration() -> Result<()> {
let tokenizer = Tokenizer::from(input);
let mut parser = Parser::new(tokenizer);
assert_eq!(
"(const item = 293.15)",
parser.parse()?.unwrap().to_string()
);
assert_eq!("(const item = 20c)", parser.parse()?.unwrap().to_string());
assert_eq!(
"(const decimal = 200.15)",
@@ -115,7 +112,7 @@ fn test_function_invocation() -> Result<()> {
#[test]
fn test_priority_expression() -> Result<()> {
let input = r#"
let x = (4);
let x = (4 + 3);
"#;
let tokenizer = Tokenizer::from(input);
@@ -123,7 +120,7 @@ fn test_priority_expression() -> Result<()> {
let expression = parser.parse()?.unwrap();
assert_eq!("(let x = 4)", expression.to_string());
assert_eq!("(let x = ((4 + 3)))", expression.to_string());
Ok(())
}
@@ -140,7 +137,7 @@ fn test_binary_expression() -> Result<()> {
assert_eq!("(((45 * 2) - (15 / 5)) + (5 ** 2))", expr.to_string());
let expr = parser!("(5 - 2) * 10;").parse()?.unwrap();
assert_eq!("((5 - 2) * 10)", expr.to_string());
assert_eq!("(((5 - 2)) * 10)", expr.to_string());
Ok(())
}
@@ -160,3 +157,133 @@ fn test_negative_literal_const() -> Result<()> {
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(())
}
#[test]
fn test_tuple_declaration() -> Result<()> {
let expr = parser!("let (x, _) = (1, 2);").parse()?.unwrap();
assert_eq!("(let (x, _) = (1, 2))", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment() -> Result<()> {
let expr = parser!("(x, y) = (1, 2);").parse()?.unwrap();
assert_eq!("((x, y) = (1, 2))", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment_with_underscore() -> Result<()> {
let expr = parser!("(x, _) = (1, 2);").parse()?.unwrap();
assert_eq!("((x, _) = (1, 2))", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_declaration_with_function_call() -> Result<()> {
let expr = parser!("let (x, y) = doSomething();").parse()?.unwrap();
assert_eq!("(let (x, y) = doSomething())", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_declaration_with_function_call_with_underscore() -> Result<()> {
let expr = parser!("let (x, _) = doSomething();").parse()?.unwrap();
assert_eq!("(let (x, _) = doSomething())", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment_with_function_call() -> Result<()> {
let expr = parser!("(x, y) = doSomething();").parse()?.unwrap();
assert_eq!("((x, y) = doSomething())", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment_with_function_call_with_underscore() -> Result<()> {
let expr = parser!("(x, _) = doSomething();").parse()?.unwrap();
assert_eq!("((x, _) = doSomething())", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_declaration_with_complex_expressions() -> Result<()> {
let expr = parser!("let (x, y) = (1 + 1, doSomething());")
.parse()?
.unwrap();
assert_eq!("(let (x, y) = ((1 + 1), doSomething()))", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment_with_complex_expressions() -> Result<()> {
let expr = parser!("(x, y) = (doSomething(), 123 / someValue.Setting);")
.parse()?
.unwrap();
assert_eq!(
"((x, y) = (doSomething(), (123 / someValue.Setting)))",
expr.to_string()
);
Ok(())
}
#[test]
fn test_tuple_declaration_all_complex_expressions() -> Result<()> {
let expr = parser!("let (x, y) = (a + b, c * d);").parse()?.unwrap();
assert_eq!("(let (x, y) = ((a + b), (c * d)))", expr.to_string());
Ok(())
}

View File

@@ -1,5 +1,7 @@
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;
@@ -243,6 +245,42 @@ impl<'a> std::fmt::Display for DeviceDeclarationExpression<'a> {
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct TupleDeclarationExpression<'a> {
pub names: Vec<Spanned<Cow<'a, str>>>,
pub value: Box<Spanned<Expression<'a>>>,
}
impl<'a> std::fmt::Display for TupleDeclarationExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names = self
.names
.iter()
.map(|n| n.node.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "(let ({}) = {})", names, self.value)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct TupleAssignmentExpression<'a> {
pub names: Vec<Spanned<Cow<'a, str>>>,
pub value: Box<Spanned<Expression<'a>>>,
}
impl<'a> std::fmt::Display for TupleAssignmentExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names = self
.names
.iter()
.map(|n| n.node.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "(({}) = {})", names, self.value)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct IfExpression<'a> {
pub condition: Box<Spanned<Expression<'a>>>,
@@ -277,50 +315,29 @@ pub struct WhileExpression<'a> {
pub body: BlockExpression<'a>,
}
#[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 {
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)]
pub struct Spanned<T> {
pub span: Span,
@@ -364,8 +381,12 @@ pub enum Expression<'a> {
MethodCall(Spanned<MethodCallExpression<'a>>),
Negation(Box<Spanned<Expression<'a>>>),
Priority(Box<Spanned<Expression<'a>>>),
Return(Box<Spanned<Expression<'a>>>),
Return(Option<Box<Spanned<Expression<'a>>>>),
Syscall(Spanned<SysCall<'a>>),
Ternary(Spanned<TernaryExpression<'a>>),
Tuple(Spanned<Vec<Spanned<Expression<'a>>>>),
TupleAssignment(Spanned<TupleAssignmentExpression<'a>>),
TupleDeclaration(Spanned<TupleDeclarationExpression<'a>>),
Variable(Spanned<Cow<'a, str>>),
While(Spanned<WhileExpression<'a>>),
}
@@ -391,10 +412,31 @@ impl<'a> std::fmt::Display for Expression<'a> {
Expression::MethodCall(e) => write!(f, "{}", e),
Expression::Negation(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::Ternary(e) => write!(f, "{}", e),
Expression::Tuple(e) => {
let items = e
.node
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "({})", items)
}
Expression::TupleAssignment(e) => write!(f, "{}", e),
Expression::TupleDeclaration(e) => write!(f, "{}", e),
Expression::Variable(id) => write!(f, "{}", id),
Expression::While(e) => write!(f, "{}", e),
}
}
}

View File

@@ -102,48 +102,6 @@ impl<'a> Token<'a> {
}
}
#[derive(Debug, PartialEq, Hash, Eq, Clone)]
pub enum Temperature {
Celsius(Number),
Fahrenheit(Number),
Kelvin(Number),
}
impl std::fmt::Display for Temperature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Temperature::Celsius(n) => write!(f, "{}°C", n),
Temperature::Fahrenheit(n) => write!(f, "{}°F", n),
Temperature::Kelvin(n) => write!(f, "{}°K", n),
}
}
}
impl Temperature {
pub fn to_kelvin(self) -> Number {
match self {
Temperature::Celsius(n) => {
let n = match n {
Number::Integer(i) => Decimal::new(i as i64, 0),
Number::Decimal(d) => d,
};
Number::Decimal(n + Decimal::new(27315, 2))
}
Temperature::Fahrenheit(n) => {
let n = match n {
Number::Integer(i) => Decimal::new(i as i64, 0),
Number::Decimal(d) => d,
};
let a = n - Decimal::new(32, 0);
let b = Decimal::new(5, 0) / Decimal::new(9, 0);
Number::Decimal(a * b + Decimal::new(27315, 2))
}
Temperature::Kelvin(n) => n,
}
}
}
macro_rules! symbol {
($var:ident) => {
|_| Symbol::$var
@@ -157,7 +115,7 @@ macro_rules! keyword {
}
#[derive(Debug, PartialEq, Hash, Eq, Clone, Logos)]
#[logos(skip r"[ \t\f]+")]
#[logos(skip r"[ \r\t\f]+")]
#[logos(extras = Extras)]
#[logos(error(LexError, LexError::from_lexer))]
pub enum TokenType<'a> {
@@ -225,6 +183,7 @@ pub enum TokenType<'a> {
#[token(".", symbol!(Dot))]
#[token("^", symbol!(Caret))]
#[token("%", symbol!(Percent))]
#[token("?", symbol!(Question))]
#[token("==", symbol!(Equal))]
#[token("!=", symbol!(NotEqual))]
#[token("&&", symbol!(LogicalAnd))]
@@ -279,30 +238,27 @@ fn parse_number<'a>(lexer: &mut Lexer<'a, TokenType<'a>>) -> Result<Number, LexE
span.end -= lexer.extras.line_start_index;
span.start -= lexer.extras.line_start_index;
let num = if clean_str.contains('.') {
Number::Decimal(
let unit = match suffix {
Some('c') => Unit::Celsius,
Some('f') => Unit::Fahrenheit,
Some('k') => Unit::Kelvin,
_ => Unit::None,
};
if clean_str.contains('.') {
Ok(Number::Decimal(
clean_str
.parse::<Decimal>()
.map_err(|_| LexError::NumberParse(line, span, slice.to_string()))?,
)
unit,
))
} else {
Number::Integer(
Ok(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)
unit,
))
}
}
@@ -394,19 +350,55 @@ impl<'a> std::fmt::Display for TokenType<'a> {
}
}
#[derive(Debug, PartialEq, Hash, Eq, Clone, Copy)]
pub enum Unit {
None,
Celsius,
Fahrenheit,
Kelvin,
}
#[derive(Debug, PartialEq, Hash, Eq, Clone, Copy)]
pub enum Number {
/// Represents an integer number
Integer(i128),
Integer(i128, Unit),
/// Represents a decimal type number with a precision of 64 bits
Decimal(Decimal),
Decimal(Decimal, Unit),
}
impl Number {
pub fn unit(&self) -> Unit {
match self {
Number::Integer(_, u) => *u,
Number::Decimal(_, u) => *u,
}
}
pub fn has_unit(&self) -> bool {
self.unit() != Unit::None
}
}
impl From<bool> for Number {
fn from(value: bool) -> Self {
Self::Integer(if value { 1 } else { 0 }, Unit::None)
}
}
impl From<Number> for Decimal {
fn from(value: Number) -> Self {
match value {
Number::Decimal(d) => d,
Number::Integer(i) => Decimal::from(i),
let (val, unit) = match value {
Number::Decimal(d, u) => (d, u),
Number::Integer(i, u) => (Decimal::from(i), u),
};
match unit {
Unit::None | Unit::Kelvin => val,
Unit::Celsius => val + Decimal::new(27315, 2),
Unit::Fahrenheit => {
(val - Decimal::new(32, 0)) * Decimal::new(5, 0) / Decimal::new(9, 0)
+ Decimal::new(27315, 2)
}
}
}
}
@@ -416,22 +408,48 @@ impl std::ops::Neg for Number {
fn neg(self) -> Self::Output {
match self {
Self::Integer(i) => Self::Integer(-i),
Self::Decimal(d) => Self::Decimal(-d),
Self::Integer(i, u) => Self::Integer(-i, u),
Self::Decimal(d, u) => Self::Decimal(-d, u),
}
}
}
fn determine_target_unit(lhs_unit: Unit, rhs_unit: Unit) -> Option<Unit> {
if lhs_unit == rhs_unit {
return Some(lhs_unit);
}
if lhs_unit != Unit::None && rhs_unit == Unit::None {
return Some(lhs_unit);
}
if lhs_unit == Unit::None && rhs_unit != Unit::None {
return Some(rhs_unit);
}
// Mismatched units (C + F) -> Fallback to Kelvin/None
None
}
impl std::ops::Add for Number {
type Output = Number;
fn add(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Self::Integer(l), Self::Integer(r)) => Number::Integer(l + r),
(Self::Decimal(l), Self::Decimal(r)) => Number::Decimal(l + r),
(Self::Integer(l), Self::Decimal(r)) => Number::Decimal(Decimal::from(l) + r),
(Self::Decimal(l), Self::Integer(r)) => Number::Decimal(l + Decimal::from(r)),
// If we can determine a common target unit (e.g. C + C = C, or C + Scalar = C),
// we preserve that unit. Otherwise, we convert to Kelvin (Decimal) and return Unit::None.
if let Some(target_unit) = determine_target_unit(self.unit(), rhs.unit()) {
return match (self, rhs) {
(Self::Integer(l, _), Self::Integer(r, _)) => Number::Integer(l + r, target_unit),
(Self::Decimal(l, _), Self::Decimal(r, _)) => Number::Decimal(l + r, target_unit),
(Self::Integer(l, _), Self::Decimal(r, _)) => {
Number::Decimal(Decimal::from(l) + r, target_unit)
}
(Self::Decimal(l, _), Self::Integer(r, _)) => {
Number::Decimal(l + Decimal::from(r), target_unit)
}
};
}
let l: Decimal = self.into();
let r: Decimal = rhs.into();
Number::Decimal(l + r, Unit::None)
}
}
@@ -439,12 +457,22 @@ impl std::ops::Sub for Number {
type Output = Number;
fn sub(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Self::Integer(l), Self::Integer(r)) => Self::Integer(l - r),
(Self::Decimal(l), Self::Integer(r)) => Self::Decimal(l - Decimal::from(r)),
(Self::Integer(l), Self::Decimal(r)) => Self::Decimal(Decimal::from(l) - r),
(Self::Decimal(l), Self::Decimal(r)) => Self::Decimal(l - r),
if let Some(target_unit) = determine_target_unit(self.unit(), rhs.unit()) {
return match (self, rhs) {
(Self::Integer(l, _), Self::Integer(r, _)) => Number::Integer(l - r, target_unit),
(Self::Decimal(l, _), Self::Decimal(r, _)) => Number::Decimal(l - r, target_unit),
(Self::Integer(l, _), Self::Decimal(r, _)) => {
Number::Decimal(Decimal::from(l) - r, target_unit)
}
(Self::Decimal(l, _), Self::Integer(r, _)) => {
Number::Decimal(l - Decimal::from(r), target_unit)
}
};
}
let l: Decimal = self.into();
let r: Decimal = rhs.into();
Number::Decimal(l - r, Unit::None)
}
}
@@ -452,12 +480,26 @@ impl std::ops::Mul for Number {
type Output = Number;
fn mul(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Number::Integer(l), Number::Integer(r)) => Number::Integer(l * r),
(Number::Integer(l), Number::Decimal(r)) => Number::Decimal(Decimal::from(l) * r),
(Number::Decimal(l), Number::Integer(r)) => Number::Decimal(l * Decimal::from(r)),
(Number::Decimal(l), Number::Decimal(r)) => Number::Decimal(l * r),
if let Some(target_unit) = determine_target_unit(self.unit(), rhs.unit()) {
return match (self, rhs) {
(Number::Integer(l, _), Number::Integer(r, _)) => {
Number::Integer(l * r, target_unit)
}
(Number::Integer(l, _), Number::Decimal(r, _)) => {
Number::Decimal(Decimal::from(l) * r, target_unit)
}
(Number::Decimal(l, _), Number::Integer(r, _)) => {
Number::Decimal(l * Decimal::from(r), target_unit)
}
(Number::Decimal(l, _), Number::Decimal(r, _)) => {
Number::Decimal(l * r, target_unit)
}
};
}
let l: Decimal = self.into();
let r: Decimal = rhs.into();
Number::Decimal(l * r, Unit::None)
}
}
@@ -465,7 +507,22 @@ impl std::ops::Div for Number {
type Output = Number;
fn div(self, rhs: Self) -> Self::Output {
Number::Decimal(Decimal::from(self) / Decimal::from(rhs))
if let Some(target_unit) = determine_target_unit(self.unit(), rhs.unit()) {
// Division always promotes to Decimal
let l_val = match self {
Self::Integer(i, _) => Decimal::from(i),
Self::Decimal(d, _) => d,
};
let r_val = match rhs {
Self::Integer(i, _) => Decimal::from(i),
Self::Decimal(d, _) => d,
};
return Number::Decimal(l_val / r_val, target_unit);
}
let l: Decimal = self.into();
let r: Decimal = rhs.into();
Number::Decimal(l / r, Unit::None)
}
}
@@ -473,15 +530,36 @@ impl std::ops::Rem for Number {
type Output = Number;
fn rem(self, rhs: Self) -> Self::Output {
Number::Decimal(Decimal::from(self) % Decimal::from(rhs))
if let Some(target_unit) = determine_target_unit(self.unit(), rhs.unit()) {
let l_val = match self {
Self::Integer(i, _) => Decimal::from(i),
Self::Decimal(d, _) => d,
};
let r_val = match rhs {
Self::Integer(i, _) => Decimal::from(i),
Self::Decimal(d, _) => d,
};
return Number::Decimal(l_val % r_val, target_unit);
}
let l: Decimal = self.into();
let r: Decimal = rhs.into();
Number::Decimal(l % r, Unit::None)
}
}
impl std::fmt::Display for Number {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Number::Integer(i) => write!(f, "{}", i),
Number::Decimal(d) => write!(f, "{}", d),
let (val, unit) = match self {
Number::Integer(i, u) => (i.to_string(), u),
Number::Decimal(d, u) => (d.to_string(), u),
};
match unit {
Unit::None => write!(f, "{}", val),
Unit::Celsius => write!(f, "{}c", val),
Unit::Fahrenheit => write!(f, "{}f", val),
Unit::Kelvin => write!(f, "{}k", val),
}
}
}
@@ -535,6 +613,8 @@ pub enum Symbol {
Caret,
/// Represents the `%` symbol
Percent,
/// Represents the `?` symbol
Question,
// Double Character Symbols
/// Represents the `==` symbol
@@ -601,6 +681,7 @@ impl std::fmt::Display for Symbol {
Self::Asterisk => write!(f, "*"),
Self::Slash => write!(f, "/"),
Self::LessThan => write!(f, "<"),
Self::Question => write!(f, "?"),
Self::LessThanOrEqual => write!(f, "<="),
Self::GreaterThan => write!(f, ">"),
Self::GreaterThanOrEqual => write!(f, ">="),
@@ -761,3 +842,21 @@ documented! {
While,
}
}
#[cfg(test)]
mod tests {
use super::TokenType;
use logos::Logos;
#[test]
fn test_windows_crlf_endings() -> anyhow::Result<()> {
let src = "let i = 0;\r\n";
let lexer = TokenType::lexer(src);
let tokens = lexer.collect::<Vec<_>>();
assert!(!tokens.iter().any(|res| res.is_err()));
Ok(())
}
}

View File

@@ -1,5 +1,5 @@
use compiler::Compiler;
use helpers::Documentation;
use compiler::{CompilationResult, Compiler};
use helpers::{Documentation, Span};
use parser::{sys_call::SysCall, Parser};
use safer_ffi::prelude::*;
use std::io::BufWriter;
@@ -8,6 +8,20 @@ use 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]
#[repr(C)]
pub struct FfiToken {
@@ -34,6 +48,17 @@ pub struct FfiDocumentedItem {
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 {
fn from(value: lsp_types::Range) -> 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]
pub fn free_ffi_token_vec(v: safer_ffi::Vec<FfiToken>) {
drop(v)
@@ -94,28 +124,56 @@ pub fn free_docs_vec(v: safer_ffi::Vec<FfiDocumentedItem>) {
/// 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#.
#[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 input = String::from_utf16_lossy(input.as_slice());
let mut writer = BufWriter::new(Vec::new());
let tokenizer = Tokenizer::from(input.as_str());
let parser = Parser::new(tokenizer);
let compiler = Compiler::new(parser, &mut writer, None);
let compiler = Compiler::new(parser, None);
if !compiler.compile().is_empty() {
return safer_ffi::String::EMPTY;
let res = compiler.compile();
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 {
return safer_ffi::String::EMPTY;
return (safer_ffi::String::EMPTY, map);
};
// 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]
@@ -180,11 +238,12 @@ pub fn diagnose_source(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<
let res = std::panic::catch_unwind(|| {
let input = String::from_utf16_lossy(input.as_slice());
let mut writer = BufWriter::new(Vec::new());
let tokenizer = Tokenizer::from(input.as_str());
let compiler = Compiler::new(Parser::new(tokenizer), &mut writer, None);
let compiler = Compiler::new(Parser::new(tokenizer), None);
let diagnosis = compiler.compile();
let CompilationResult {
errors: diagnosis, ..
} = compiler.compile();
let mut result_vec: Vec<FfiDiagnostic> = Vec::with_capacity(diagnosis.len());

View File

@@ -1,7 +1,7 @@
#![allow(clippy::result_large_err)]
use clap::Parser;
use compiler::Compiler;
use compiler::{CompilationResult, Compiler};
use parser::Parser as ASTParser;
use std::{
fs::File,
@@ -53,6 +53,9 @@ struct Args {
/// The output file for the compiled program. If not set, output will go to stdout.
#[arg(short, long)]
output_file: Option<PathBuf>,
/// Should Slang attempt to optimize the output?
#[arg(short = 'z', long)]
optimize: bool,
}
fn run_logic<'a>() -> Result<(), Error<'a>> {
@@ -88,9 +91,13 @@ fn run_logic<'a>() -> Result<(), Error<'a>> {
None => BufWriter::new(Box::new(std::io::stdout())),
};
let compiler = Compiler::new(parser, &mut writer, None);
let compiler = Compiler::new(parser, None);
let errors = compiler.compile();
let CompilationResult {
errors,
instructions,
..
} = compiler.compile();
if !errors.is_empty() {
let mut std_error = stderr();
@@ -103,6 +110,12 @@ fn run_logic<'a>() -> Result<(), Error<'a>> {
}
}
if args.optimize {
optimizer::optimize(instructions).write(&mut writer)?;
} else {
instructions.write(&mut writer)?;
}
writer.flush()?;
Ok(())