107 Commits
0.2.3 ... 0.5.1

Author SHA1 Message Date
d8fe9a0d7d Merge pull request 'optimizer-bug' (#14) from optimizer-bug into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 36s
CI/CD Pipeline / build (push) Successful in 1m44s
CI/CD Pipeline / release (push) Successful in 4s
Reviewed-on: #14
2026-01-01 12:41:11 -07:00
089fe46d36 update changelog and version bump
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 37s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2026-01-01 12:38:53 -07:00
14c641797a Fixed optimizer bug where certain syscalls were not included in register liveness analysis 2026-01-01 12:35:57 -07:00
fb5eacea02 Merge pull request 'Updated auto-doc formatting for markdown files' (#13) from doc-formatting-fixes into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 36s
CI/CD Pipeline / build (push) Successful in 1m44s
CI/CD Pipeline / release (push) Has been skipped
Reviewed-on: #13
2026-01-01 02:43:50 -07:00
9fd3a55182 Updated auto-doc formatting for markdown files
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 37s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2026-01-01 02:43:07 -07:00
397aa47217 Merge pull request '0.5.0 -- tuples and more optimizations' (#12) from 43-tuple-return into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 37s
CI/CD Pipeline / build (push) Successful in 1m45s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #12
2025-12-31 17:03:50 -07:00
6f86563863 Update changelog
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 36s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-31 17:01:26 -07:00
352041746c More tests, renamed files from slang -> stlg
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 37s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-31 14:39:38 -07:00
5f4335dbcc Added another complex stlg test file 2025-12-31 14:12:19 -07:00
2a5dfd9ab6 Ready for in-game testing
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 37s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-31 03:08:41 -07:00
2dfe36f8be more optimizations 2025-12-31 02:39:57 -07:00
d28cdfcc7b Improve dead code elimination in optimizer
- Refactored dead_store_elimination to separate forward and backward passes
- Improved register forwarding to better detect backward jumps
- Fixed handling of JumpAndLink instructions in register tracking
- Updated optimizer snapshots to reflect improved code generation

The forward pass now correctly eliminates writes that are immediately overwritten.
Register forwarding now better handles conditional branches and loops.

Note: Backward pass for dead code elimination disabled for now - it needs
additional work to properly handle return values and call site analysis.
2025-12-31 02:37:26 -07:00
95c17b563c More optimizer fixes
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 2m2s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-30 23:34:14 -07:00
dbc4c72c3b Add .snap.new files to gitignore 2025-12-30 23:00:55 -07:00
964ad92077 More compiler optimizations 2025-12-30 23:00:17 -07:00
63f55b66cb More optimizations 2025-12-30 22:24:47 -07:00
d19a53bbee More optimizations and snapshot integration tests 2025-12-30 21:20:46 -07:00
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
95 changed files with 12792 additions and 1559 deletions

View File

@@ -4,6 +4,7 @@ name: CI/CD Pipeline
on: on:
push: push:
branches: ["master"] branches: ["master"]
tags: ["*.*.*"]
pull_request: pull_request:
branches: ["master"] branches: ["master"]
@@ -57,6 +58,10 @@ jobs:
slang-builder \ slang-builder \
./build.sh ./build.sh
- name: Zip Workshop Folder
run: |
zip -r release/workshop.zip release/workshop/
# 3. Fix Permissions # 3. Fix Permissions
# Docker writes files as root. We need to own them to upload them. # Docker writes files as root. We need to own them to upload them.
- name: Fix Permissions - name: Fix Permissions
@@ -65,7 +70,36 @@ jobs:
# 4. Upload to GitHub # 4. Upload to GitHub
- name: Upload Release Artifacts - name: Upload Release Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: StationeersSlang-Release name: StationeersSlang-Release
path: 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 }}

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

@@ -0,0 +1,239 @@
# 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
!IMPORTANT: make sure you use these commands instead of creating temporary files.
```bash
cd rust_compiler
# Compile Slang code to IC10 using current compiler changes
echo 'let x = 5;' | cargo run --bin slang
# Compile Slang code to IC10 with optimization
echo 'let x = 5;' | cargo run --bin slang -z
# 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

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
target target
*.ic10 *.ic10
*.snap.new
release release
csharp_mod/bin csharp_mod/bin
obj obj

View File

@@ -1,5 +1,111 @@
# Changelog # Changelog
[0.5.1]
- Fixed optimizer bug where `StoreBatch` and `StoreBatchNamed` instructions
were not recognized as reading operands, causing incorrect elimination of
necessary device property loads
- Added comprehensive register read tracking for `StoreSlot`, `JumpRelative`,
and `Alias` instructions in the optimizer
[0.5.0]
- Added full tuple support: declarations, assignments, and returns
- Refactored optimizer into modular passes with improved code generation
- Enhanced peephole optimizations and pattern recognition
- Comprehensive test coverage for edge cases and error handling
[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] [0.2.3]
- Fixed stack underflow with function invocations - Fixed stack underflow with function invocations

View File

@@ -2,11 +2,11 @@
<ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Slang</Name> <Name>Slang</Name>
<Author>JoeDiertay</Author> <Author>JoeDiertay</Author>
<Version>0.2.3</Version> <Version>0.5.1</Version>
<Description> <Description>
[h1]Slang: High-Level Programming for Stationeers[/h1] [h1]Slang: High-Level Programming for Stationeers[/h1]
Stop writing assembly. Start writing code. 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. 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] [h2]Features[/h2]
[list] [list]
[*] [b]In-Game Compilation:[/b] No external tools needed. Write Slang directly in the IC editor. [*] [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]Control Flow:[/b] Write readable logic with if, else, while, loop, break, and continue.
[*] [b]Functions:[/b] Create reusable code blocks with arguments. [*] [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. [*] [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]Optimizations:[/b] Features like Constant Folding calculate math at compile time to save instructions.
[*] [b]Device Aliasing:[/b] Simple mapping: device sensor = "d0". [*] [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]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] [/list]
[h2]Installation[/h2] [h2]Installation[/h2]
@@ -50,19 +52,14 @@ loop {
[h2]Known Issues (Beta)[/h2] [h2]Known Issues (Beta)[/h2]
[list] [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. A workaround is being 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, 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.
[*] [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.
[/list] [/list]
[h2]Planned Features[/h2] [h2]Planned Features[/h2]
[list] [list]
[*] Side-by-side view: Slang vs. Compiled IC10.
[*] Compiler optimizations (dead code elimination, smarter register allocation).
[*] Enhanced LSP features (Autocomplete, Go to Definition). [*] Enhanced LSP features (Autocomplete, Go to Definition).
[*] Full feature parity with all IC10 instructions. [*] Full feature parity with all IC10 instructions.
[*] Tutorials and beginner script examples.
[/list] [/list]
[h2]FAQ[/h2] [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] [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. 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] [h2]Useful Links[/h2]
[url=https://github.com/dbidwell94/stationeers_lang]Source Code on GitHub[/url] [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/stationeers]Stationeers Official Discord[/url]
[url=https://discord.gg/M4sCfYMacs]Stationeers Modding 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> </Description>
<ChangeLog xsi:nil="true" /> <ChangeLog xsi:nil="true" />
<WorkshopHandle>3619985558</WorkshopHandle> <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> <Tag>Quality of Life</Tag>
</Tags> </Tags>
<DependsOn WorkshopHandle="3592775931" /> <DependsOn WorkshopHandle="3592775931" />
<OrderBefore WorkshopHandle="3592775931" /> <OrderAfter WorkshopHandle="3592775931" />
<InGameDescription><![CDATA[ <InGameDescription><![CDATA[
<size=30><color=#ffff00>Slang - High Level Language Compiler</color></size> <size=30><color=#ffff00>Slang - High Level Language Compiler</color></size>
A modern programming experience for Stationeers. Write C-style code that compiles to IC10 instantly. 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 See: https://github.com/StationeersLaunchPad/StationeersLaunchPad
Source Code: https://github.com/dbidwell94/stationeers_lang Source Code: https://github.com/dbidwell94/stationeers_lang
Documentation: https://github.com/dbidwell94/stationeers_lang/blob/master/docs/getting-started.md
]]></InGameDescription> ]]></InGameDescription>
</ModMetadata> </ModMetadata>

View File

@@ -1,30 +1,45 @@
# Stationeers Language (slang) # Slang Language Documentation
This is an ambitious attempt at creating: 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
- A new programming language (slang) of in-game IC10.
- A compiler to translate slang -> IC10
- A mod to allow direct input of slang in the in-game script editor to ## Quick Links
automatically compile to IC10 before running
- [Getting Started](docs/getting-started.md) - Installation and first program
This project currently outputs 3 files: - [Language Reference](docs/language-reference.md) - Complete syntax guide
- [Built-in Functions](docs/builtins.md) - System calls and math functions
- A Linux CLI - [Examples](docs/examples.md) - Real-world code samples
- A Windows CLI
- A Windows FFI dll ## Overview
- Contains a single function: `compile_from_string`
Slang aims to reduce the time spent writing IC10 assembly by providing:
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 - **Familiar syntax** - C-like declarations, control flow, and expressions
whatever you are working on. This project is NOT meant to fully replace IC10. - **Device abstraction** - Named device bindings with property access
Obviously hand-coded assembly written by an experienced programmer is more - **Automatic register allocation** - No manual register management
optimized and smaller than something that a C compiler will spit out. This is - **Built-in functions** - Math operations and device I/O as function calls
the same way. It WILL produce valid IC10, but for large complicated projects it - **Temperature literals** - Native support for Celsius, Fahrenheit, and Kelvin
might produce over the allowed limit of lines the in-game editor supports.
## Example
Current Unknowns
```rust
- Should I support a configurable script line length in-game to allow larger device gasSensor = "d0";
scripts to be saved? device airCon = "d1";
- Should compilation be "behind the scenes" (in game editor will ALWAYS be what
you put in. IC10 will be IC10, slang will be slang) 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; return toReturn;
} }
public static unsafe List<SourceMapEntry> ToList(this Vec_FfiSourceMapEntry_t vec)
{
var toReturn = new List<SourceMapEntry>((int)vec.len);
var currentPtr = vec.ptr;
for (int i = 0; i < (int)vec.len; i++)
{
var item = currentPtr[i];
toReturn.Add(
new SourceMapEntry
{
Ic10Line = item.line_number,
SlangSource = new Range
{
EndCol = item.span.end_col,
EndLine = item.span.end_line,
StartCol = item.span.start_col,
StartLine = item.span.start_line,
},
}
);
}
return toReturn;
}
private static uint GetColorForKind(uint kind) private static uint GetColorForKind(uint kind)
{ {
switch (kind) switch (kind)

View File

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

View File

@@ -9,10 +9,33 @@ using StationeersIC10Editor;
public class SlangFormatter : ICodeFormatter public class SlangFormatter : ICodeFormatter
{ {
public const string SLANG_SRC = "SLANG_SRC";
private CancellationTokenSource? _lspCancellationToken; private CancellationTokenSource? _lspCancellationToken;
private object _tokenLock = new(); 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 ColorControl = ColorFromHTML("#C586C0"); // Pink (if, return, loop)
public static readonly uint ColorDeclaration = ColorFromHTML("#569CD6"); // Blue (let, device, fn) public static readonly uint ColorDeclaration = ColorFromHTML("#569CD6"); // Blue (let, device, fn)
public static readonly uint ColorFunction = ColorFromHTML("#DCDCAA"); // Yellow (syscalls) 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 ColorBoolean = ColorFromHTML("#569CD6"); // Blue (true/false)
public static readonly uint ColorIdentifier = ColorFromHTML("#9CDCFE"); // Light Blue (variables) public static readonly uint ColorIdentifier = ColorFromHTML("#9CDCFE"); // Light Blue (variables)
public static new readonly uint ColorDefault = ColorFromHTML("#D4D4D4"); // White (punctuation ; { } ) 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"); public static readonly uint ColorOperator = ColorFromHTML("#D4D4D4");
#endregion
private HashSet<uint> _linesWithErrors = new(); private HashSet<uint> _linesWithErrors = new();
private int _lastLineCount = -1; private int _lastLineCount = -1;
@@ -33,6 +54,7 @@ public class SlangFormatter : ICodeFormatter
: base() : base()
{ {
OnCodeChanged += HandleCodeChanged; OnCodeChanged += HandleCodeChanged;
OnCaretMoved += UpdateIc10Formatter;
} }
public static double MatchingScore(string input) public static double MatchingScore(string input)
@@ -41,6 +63,11 @@ public class SlangFormatter : ICodeFormatter
if (string.IsNullOrWhiteSpace(input)) if (string.IsNullOrWhiteSpace(input))
return 0d; return 0d;
if (input.Contains(SLANG_SRC))
{
return 1.0;
}
// Run the compiler to get diagnostics // Run the compiler to get diagnostics
var diagnostics = Marshal.DiagnoseSource(input); var diagnostics = Marshal.DiagnoseSource(input);
@@ -67,7 +94,28 @@ public class SlangFormatter : ICodeFormatter
public override string Compile() 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) public override StyledLine ParseLine(string line)
@@ -86,6 +134,10 @@ public class SlangFormatter : ICodeFormatter
return styledLine; 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() private void HandleCodeChanged()
{ {
CancellationToken token; CancellationToken token;
@@ -101,6 +153,11 @@ public class SlangFormatter : ICodeFormatter
_ = HandleLsp(inputSrc, token); _ = 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) private async Task HandleLsp(string inputSrc, CancellationToken cancellationToken)
{ {
try try
@@ -126,6 +183,29 @@ public class SlangFormatter : ICodeFormatter
); );
ApplyDiagnostics(dict); 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 (OperationCanceledException) { }
catch (Exception ex) 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) private void ApplyDiagnostics(Dictionary<uint, IGrouping<uint, Diagnostic>> dict)
{ {
HashSet<uint> linesToRefresh; HashSet<uint> linesToRefresh;

View File

@@ -1,99 +1,69 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace Slang; namespace Slang;
public static class GlobalCode 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:"; public const string SLANG_SRC = "#SLANG_SRC:";
// This is a Dictionary of ENCODED source code, compressed /// <summary>
// so that save file data is smaller /// This Dictionary stores the source maps for the given SLANG_REF, where
private static Dictionary<Guid, string> codeDict = new(); /// 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) foreach (var entry in sourceMapEntries)
{
if (!codeDict.ContainsKey(reference))
{ {
return string.Empty; if (!builtDictionary.ContainsKey(entry.Ic10Line))
}
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))
{ {
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); if (!sourceMaps[reference].ContainsKey(icErrorLine))
using (var memoryStream = new MemoryStream(compressedBytes))
{ {
using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) return false;
{
using (var outputStream = new MemoryStream())
{
gzipStream.CopyTo(outputStream);
return Encoding.UTF8.GetString(outputStream.ToArray());
}
}
} }
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 struct Range
{ {
public uint StartCol; public uint StartCol = 0;
public uint EndCol; public uint EndCol = 0;
public uint StartLine; public uint StartLine = 0;
public uint EndLine; public uint EndLine = 0;
public Range(uint startLine, uint startCol, uint endLine, uint endCol)
{
StartLine = startLine;
StartCol = startCol;
EndLine = endLine;
EndCol = endCol;
}
public override string ToString()
{
return $"L{StartLine}C{StartCol} - L{EndLine}C{EndCol}";
}
} }
public struct Diagnostic public struct Diagnostic
@@ -23,6 +36,17 @@ public struct Diagnostic
public Range Range; public Range Range;
} }
public struct SourceMapEntry
{
public Range SlangSource;
public uint Ic10Line;
public override string ToString()
{
return $"IC10: {Ic10Line} Slang: `{SlangSource}`";
}
}
public static class Marshal public static class Marshal
{ {
private static IntPtr _libraryHandle = IntPtr.Zero; private static IntPtr _libraryHandle = IntPtr.Zero;
@@ -43,7 +67,9 @@ public static class Marshal
try try
{ {
_libraryHandle = LoadLibrary(ExtractNativeLibrary(Ffi.RustLib)); _libraryHandle = LoadLibrary(ExtractNativeLibrary(Ffi.RustLib));
L.Debug("Rust DLL loaded successfully. Enjoy native speed compilations!");
CodeFormatters.RegisterFormatter("Slang", typeof(SlangFormatter), true); CodeFormatters.RegisterFormatter("Slang", typeof(SlangFormatter), true);
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
@@ -67,8 +93,13 @@ public static class Marshal
try try
{ {
FreeLibrary(_libraryHandle); CodeFormatters.RegisterFormatter("Slang", typeof(PlainTextFormatter), true);
if (!FreeLibrary(_libraryHandle))
{
L.Warning("Unable to free Rust library");
}
_libraryHandle = IntPtr.Zero; _libraryHandle = IntPtr.Zero;
L.Debug("Rust DLL library freed");
return true; return true;
} }
catch (Exception ex) 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()) if (String.IsNullOrEmpty(inputString) || !EnsureLibLoaded())
{ {
compiledString = String.Empty; compiledString = String.Empty;
sourceMapEntries = new();
return false; return false;
} }
@@ -95,19 +131,16 @@ public static class Marshal
}; };
var result = Ffi.compile_from_string(input); var result = Ffi.compile_from_string(input);
try try
{ {
if ((ulong)result.len < 1) sourceMapEntries = result.source_map.ToList();
{ compiledString = result.output_code.AsString();
compiledString = String.Empty;
return false;
}
compiledString = result.AsString();
return true; return true;
} }
finally finally
{ {
result.Drop(); Ffi.free_ffi_compilation_result(result);
} }
} }
} }
@@ -165,9 +198,9 @@ public static class Marshal
Assembly assembly = Assembly.GetExecutingAssembly(); Assembly assembly = Assembly.GetExecutingAssembly();
using (Stream stream = assembly.GetManifestResourceStream(libName)) using (Stream resourceStream = assembly.GetManifestResourceStream(libName))
{ {
if (stream == null) if (resourceStream == null)
{ {
L.Error( L.Error(
$"{libName} not found. This means it was not embedded in the mod. Please contact the mod author!" $"{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 ""; 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 try
{ {
using (FileStream fileStream = new FileStream(destinationPath, FileMode.Create)) using (FileStream fileStream = new FileStream(destinationPath, FileMode.Create))
{ {
stream.CopyTo(fileStream); resourceStream.CopyTo(fileStream);
} }
return destinationPath; return destinationPath;
} }
catch (IOException e) catch (IOException e)
{ {
L.Warning($"Could not overwrite {libName} (it might be in use): {e.Message}"); // If we fail here, the file is likely locked.
return ""; // 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,235 +1,11 @@
namespace Slang; namespace Slang;
using System;
using Assets.Scripts.Objects;
using Assets.Scripts.Objects.Electrical;
using Assets.Scripts.Objects.Motherboards;
using Assets.Scripts.UI; using Assets.Scripts.UI;
using HarmonyLib; using HarmonyLib;
[HarmonyPatch] [HarmonyPatch]
public static class SlangPatches public static class SlangPatches
{ {
private static ProgrammableChipMotherboard? _currentlyEditingMotherboard;
private static AsciiString? _motherboardCachedCode;
private static Guid? _currentlyEditingGuid;
[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 = _currentlyEditingGuid ?? Guid.NewGuid();
// Ensure we cache this compiled code for later retreival.
GlobalCode.SetSource(thisRef, result);
_currentlyEditingGuid = null;
// 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;
}
_currentlyEditingGuid = sourceRef;
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;
_currentlyEditingGuid = null;
}
[HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))] [HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))]
[HarmonyPostfix] [HarmonyPostfix]
public static void Stationpedia_Regenerate() public static void Stationpedia_Regenerate()

View File

@@ -1,7 +1,5 @@
using System.Text.RegularExpressions;
using BepInEx; using BepInEx;
using HarmonyLib; using HarmonyLib;
using LaunchPadBooster;
namespace Slang namespace Slang
{ {
@@ -41,45 +39,32 @@ namespace Slang
{ {
public const string PluginGuid = "com.biddydev.slang"; public const string PluginGuid = "com.biddydev.slang";
public const string PluginName = "Slang"; public const string PluginName = "Slang";
public const string PluginVersion = "0.1.1"; public const string PluginVersion = "0.5.1";
public static Mod MOD = new Mod(PluginName, PluginVersion); private static Harmony? _harmony;
private Harmony? _harmony; public void Awake()
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()
{ {
L.SetLogger(Logger); 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, // 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. // as the compiler itself has failed to load.
if (!Marshal.Init()) if (!Marshal.Init())
{ {
L.Error("Marshal failed to init");
return; 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

@@ -1,59 +1,73 @@
using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Slang; namespace Slang;
public static class TextMeshProFormatter public static class TextMeshProFormatter
{ {
private const string CODE_COLOR = "#FFD700"; private const string CODE_COLOR = "#FFD700"; // Gold
private const string LINK_COLOR = "#0099FF"; // Blue
private const string QUOTE_COLOR = "#90EE90"; // Light Green
public static string FromMarkdown(string markdown) public static string FromMarkdown(string markdown)
{ {
if (string.IsNullOrEmpty(markdown)) if (string.IsNullOrEmpty(markdown))
return ""; return "";
// 1. Normalize Line Endings // Normalize Line Endings
string text = markdown.Replace("\r\n", "\n"); string text = markdown.Replace("\r\n", "\n");
// 2. Handle Code Blocks (```) // Process code blocks FIRST (``` ... ```)
text = Regex.Replace( text = Regex.Replace(
text, text,
@"```\s*(.*?)\s*```", @"```[^\n]*\n(.*?)\n```",
match => match =>
{ {
var codeContent = match.Groups[1].Value; var codeContent = match.Groups[1].Value;
return $"<color={CODE_COLOR}>{codeContent}</color>"; // Gold color for code return $"<color={CODE_COLOR}>{codeContent}</color>";
}, },
RegexOptions.Singleline RegexOptions.Singleline
); );
// Process headers - check for 1-6 hashes
text = Regex.Replace(text, @"^#{1}\s+(.+)$", "<size=120%><b>$1</b></size>", RegexOptions.Multiline);
text = Regex.Replace(text, @"^#{2}\s+(.+)$", "<size=110%><b>$1</b></size>", RegexOptions.Multiline);
text = Regex.Replace(text, @"^#{3}\s+(.+)$", "<size=100%><b>$1</b></size>", RegexOptions.Multiline);
text = Regex.Replace(text, @"^#{4}\s+(.+)$", "<size=90%><b>$1</b></size>", RegexOptions.Multiline);
text = Regex.Replace(text, @"^#{5}\s+(.+)$", "<size=80%><b>$1</b></size>", RegexOptions.Multiline);
text = Regex.Replace(text, @"^#{6}\s+(.+)$", "<size=70%><b>$1</b></size>", RegexOptions.Multiline);
// Process markdown links [text](url)
text = Regex.Replace( text = Regex.Replace(
text, text,
@"^\s*##\s+(.+)$", @"\[([^\]]+)\]\(([^\)]+)\)",
"<size=110%><color=#ffffff><b>$1</b></color></size>", $"<color={LINK_COLOR}><u>$1</u></color>"
RegexOptions.Multiline
); );
// 3. Handle # Headers SECOND (General) // Process inline code (`code`)
text = Regex.Replace(
text,
@"^\s*#\s+(.+)$",
"<size=120%><color=#ffffff><b>$1</b></color></size>",
RegexOptions.Multiline
);
// 4. Handle Inline Code (`code`)
text = Regex.Replace(text, @"`([^`]+)`", $"<color={CODE_COLOR}>$1</color>"); text = Regex.Replace(text, @"`([^`]+)`", $"<color={CODE_COLOR}>$1</color>");
// 5. Handle Bold (**text**) // Process bold (**text**)
text = Regex.Replace(text, @"\*\*(.+?)\*\*", "<b>$1</b>"); text = Regex.Replace(text, @"\*\*(.+?)\*\*", "<b>$1</b>");
// 6. Handle Italics (*text*) // Process italics (*text*)
text = Regex.Replace(text, @"\*(.+?)\*", "<i>$1</i>"); text = Regex.Replace(text, @"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", "<i>$1</i>");
// 7. Convert Newlines to TMP Line Breaks // Process block quotes (> text)
// Stationpedia needs <br> or explicit newlines. text = Regex.Replace(
// Often just ensuring \n is preserved is enough, but <br> is safer for HTML-like parsers. text,
text = text.Replace("\n", "<br>"); @"^>\s+(.+)$",
$"<color={QUOTE_COLOR}><i>$1</i></color>",
RegexOptions.Multiline
);
// Process unordered lists (- items)
text = Regex.Replace(
text,
@"^-\s+(.+)$",
" • $1",
RegexOptions.Multiline
);
return text; return text;
} }

View File

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

262
rust_compiler/Cargo.lock generated
View File

@@ -23,7 +23,7 @@ version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.16",
"once_cell", "once_cell",
"version_check", "version_check",
] ]
@@ -73,7 +73,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [ dependencies = [
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -84,7 +84,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell_polyfill", "once_cell_polyfill",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -135,6 +135,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]] [[package]]
name = "bitvec" name = "bitvec"
version = "1.0.1" version = "1.0.1"
@@ -172,9 +178,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.0" version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]] [[package]]
name = "bytecheck" name = "bytecheck"
@@ -268,6 +274,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"helpers", "helpers",
"il",
"indoc", "indoc",
"lsp-types", "lsp-types",
"parser", "parser",
@@ -277,6 +284,18 @@ dependencies = [
"tokenizer", "tokenizer",
] ]
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -292,12 +311,28 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "ext-trait" name = "ext-trait"
version = "1.0.1" version = "1.0.1"
@@ -333,13 +368,19 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320bea982e85d42441eb25c49b41218e7eaa2657e8f90bc4eca7437376751e23" checksum = "320bea982e85d42441eb25c49b41218e7eaa2657e8f90bc4eca7437376751e23"
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "fluent-uri" name = "fluent-uri"
version = "0.1.4" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
] ]
[[package]] [[package]]
@@ -365,6 +406,18 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.32.3" version = "0.32.3"
@@ -397,6 +450,15 @@ name = "helpers"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"lsp-types",
]
[[package]]
name = "il"
version = "0.1.0"
dependencies = [
"helpers",
"rust_decimal",
] ]
[[package]] [[package]]
@@ -418,6 +480,32 @@ dependencies = [
"rustversion", "rustversion",
] ]
[[package]]
name = "insta"
version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "983e3b24350c84ab8a65151f537d67afbbf7153bb9f1110e03e9fa9b07f67a5c"
dependencies = [
"console",
"once_cell",
"similar",
"tempfile",
]
[[package]]
name = "integration_tests"
version = "0.1.0"
dependencies = [
"anyhow",
"compiler",
"il",
"indoc",
"insta",
"optimizer",
"parser",
"tokenizer",
]
[[package]] [[package]]
name = "inventory" name = "inventory"
version = "0.3.21" version = "0.3.21"
@@ -455,6 +543,12 @@ version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]] [[package]]
name = "logos" name = "logos"
version = "0.16.0" version = "0.16.0"
@@ -495,7 +589,7 @@ version = "0.97.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"fluent-uri", "fluent-uri",
"serde", "serde",
"serde_json", "serde_json",
@@ -563,6 +657,16 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "optimizer"
version = "0.1.0"
dependencies = [
"anyhow",
"helpers",
"il",
"rust_decimal",
]
[[package]] [[package]]
name = "parser" name = "parser"
version = "0.1.0" version = "0.1.0"
@@ -571,6 +675,7 @@ dependencies = [
"helpers", "helpers",
"lsp-types", "lsp-types",
"pretty_assertions", "pretty_assertions",
"safer-ffi",
"thiserror", "thiserror",
"tokenizer", "tokenizer",
] ]
@@ -657,6 +762,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "radium" name = "radium"
version = "0.7.0" version = "0.7.0"
@@ -690,7 +801,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.16",
] ]
[[package]] [[package]]
@@ -779,6 +890,19 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags 2.10.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
@@ -907,15 +1031,22 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]] [[package]]
name = "slang" name = "slang"
version = "0.2.3" version = "0.5.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"compiler", "compiler",
"helpers", "helpers",
"lsp-types", "lsp-types",
"optimizer",
"parser", "parser",
"rust_decimal", "rust_decimal",
"safer-ffi", "safer-ffi",
@@ -992,6 +1123,19 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.17" version = "2.0.17"
@@ -1041,18 +1185,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" checksum = "fe3cea6b2aa3b910092f6abd4053ea464fab5f9c170ba5e9a6aead16ec4af2b6"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "toml_edit" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime",
@@ -1062,9 +1206,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_parser" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" checksum = "4c03bee5ce3696f31250db0bbaff18bc43301ce0e8db2ed1f07cbb2acf89984c"
dependencies = [ dependencies = [
"winnow", "winnow",
] ]
@@ -1118,6 +1262,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.106" version = "0.2.106"
@@ -1169,6 +1322,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@@ -1178,6 +1340,70 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.14" version = "0.7.14"
@@ -1187,6 +1413,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]] [[package]]
name = "with_builtin_macros" name = "with_builtin_macros"
version = "0.0.3" version = "0.0.3"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "slang" name = "slang"
version = "0.2.3" version = "0.5.1"
edition = "2021" edition = "2021"
[workspace] [workspace]
@@ -9,9 +9,10 @@ members = ["libs/*"]
[workspace.dependencies] [workspace.dependencies]
thiserror = "2" thiserror = "2"
rust_decimal = "1" rust_decimal = "1"
safer-ffi = { version = "0.1" } # Safely share structs in memory between C# and Rust safer-ffi = { version = "0.1" } # Safely share structs in memory between C# and Rust
lsp-types = { version = "0.97" } # Allows for LSP style reporting to the frontend lsp-types = { version = "0.97" } # Allows for LSP style reporting to the frontend
crc32fast = "1.5" # This is for `HASH(..)` calls to be optimized away crc32fast = "1.5" # This is for `HASH(..)` calls to be optimized away
anyhow = { version = "^1.0", features = ["backtrace"] }
[features] [features]
headers = ["safer-ffi/headers"] headers = ["safer-ffi/headers"]
@@ -42,7 +43,8 @@ tokenizer = { path = "libs/tokenizer" }
parser = { path = "libs/parser" } parser = { path = "libs/parser" }
compiler = { path = "libs/compiler" } compiler = { path = "libs/compiler" }
helpers = { path = "libs/helpers" } helpers = { path = "libs/helpers" }
optimizer = { path = "libs/optimizer" }
safer-ffi = { workspace = true } safer-ffi = { workspace = true }
anyhow = { version = "^1.0", features = ["backtrace"] } anyhow = { workspace = true }
[dev-dependencies] [dev-dependencies]

View File

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

View File

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

View File

@@ -1,24 +1,29 @@
use crate::compile;
use anyhow::Result; use anyhow::Result;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn simple_binary_expression() -> Result<()> { fn simple_binary_expression() -> Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let i = 1 + 2; let i = 1 + 2;
" "
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 3 #i move r8 3
" "
} }
); );
@@ -28,8 +33,8 @@ fn simple_binary_expression() -> Result<()> {
#[test] #[test]
fn nested_binary_expressions() -> Result<()> { fn nested_binary_expressions() -> Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
fn calculateArgs(arg1, arg2, arg3) { fn calculateArgs(arg1, arg2, arg3) {
return (arg1 + arg2) * arg3; return (arg1 + arg2) * arg3;
@@ -39,33 +44,39 @@ fn nested_binary_expressions() -> Result<()> {
" "
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
calculateArgs: calculateArgs:
pop r8 #arg3 pop r8
pop r9 #arg2 pop r9
pop r10 #arg1 pop r10
push sp
push ra push ra
add r1 r10 r9 add r1 r10 r9
mul r2 r1 r8 mul r2 r1 r8
move r15 r2 move r15 r2
j L1 j __internal_L1
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
main: main:
push 10 push 10
push 20 push 20
push 30 push 30
jal calculateArgs jal calculateArgs
move r1 r15 #__binary_temp_3 move r1 r15
add r2 r1 100 add r2 r1 100
move r8 r2 #returned move r8 r2
" "
} }
); );
@@ -75,20 +86,26 @@ fn nested_binary_expressions() -> Result<()> {
#[test] #[test]
fn stress_test_constant_folding() -> Result<()> { fn stress_test_constant_folding() -> Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let negationHell = (-1 + -2) * (-3 + (-4 * (-5 + -6))); let negationHell = (-1 + -2) * (-3 + (-4 * (-5 + -6)));
" "
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 -123 #negationHell move r8 -123
" "
} }
); );
@@ -98,16 +115,22 @@ fn stress_test_constant_folding() -> Result<()> {
#[test] #[test]
fn test_constant_folding_with_variables_mixed_in() -> Result<()> { fn test_constant_folding_with_variables_mixed_in() -> Result<()> {
let compiled = compile! { let result = compile! {
debug check
r#" r#"
device self = "db"; device self = "db";
let i = 1 - 3 * (1 + 123.4) * self.Setting + 245c; let i = 1 - 3 * (1 + 123.4) * self.Setting + 245c;
"# "#
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
@@ -116,7 +139,7 @@ fn test_constant_folding_with_variables_mixed_in() -> Result<()> {
mul r2 373.2 r1 mul r2 373.2 r1
sub r3 1 r2 sub r3 1 r2
add r4 r3 518.15 add r4 r3 518.15
move r8 r4 #i move r8 r4
" "
} }
); );
@@ -126,22 +149,28 @@ fn test_constant_folding_with_variables_mixed_in() -> Result<()> {
#[test] #[test]
fn test_ternary_expression() -> Result<()> { fn test_ternary_expression() -> Result<()> {
let compiled = compile! { let result = compile! {
debug check
r#" r#"
let i = 1 > 2 ? 15 : 20; let i = 1 > 2 ? 15 : 20;
"# "#
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
sgt r1 1 2 sgt r1 1 2
select r2 r1 15 20 select r2 r1 15 20
move r8 r2 #i move r8 r2
" "
} }
); );
@@ -151,24 +180,91 @@ fn test_ternary_expression() -> Result<()> {
#[test] #[test]
fn test_ternary_expression_assignment() -> Result<()> { fn test_ternary_expression_assignment() -> Result<()> {
let compiled = compile! { let result = compile! {
debug check
r#" r#"
let i = 0; let i = 0;
i = 1 > 2 ? 15 : 20; i = 1 > 2 ? 15 : 20;
"# "#
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 0 #i move r8 0
sgt r1 1 2 sgt r1 1 2
select r2 r1 15 20 select r2 r1 15 20
move r8 r2 #i 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 indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn test_if_statement() -> anyhow::Result<()> { fn test_if_statement() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let a = 10; let a = 10;
if (a > 5) { 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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 10 #a move r8 10
sgt r1 r8 5 sgt r1 r8 5
beq r1 0 L1 beqz r1 __internal_L1
move r8 20 #a move r8 20
L1: __internal_L1:
" "
} }
); );
@@ -34,8 +39,8 @@ fn test_if_statement() -> anyhow::Result<()> {
#[test] #[test]
fn test_if_else_statement() -> anyhow::Result<()> { fn test_if_else_statement() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let a = 0; let a = 0;
if (10 > 5) { 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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 0 #a move r8 0
sgt r1 10 5 sgt r1 10 5
beq r1 0 L2 beqz r1 __internal_L2
move r8 1 #a move r8 1
j L1 j __internal_L1
L2: __internal_L2:
move r8 2 #a move r8 2
L1: __internal_L1:
" "
} }
); );
@@ -69,8 +80,8 @@ fn test_if_else_statement() -> anyhow::Result<()> {
#[test] #[test]
fn test_if_else_if_statement() -> anyhow::Result<()> { fn test_if_else_if_statement() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let a = 0; let a = 0;
if (a == 1) { 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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 0 #a move r8 0
seq r1 r8 1 seq r1 r8 1
beq r1 0 L2 beqz r1 __internal_L2
move r8 10 #a move r8 10
j L1 j __internal_L1
L2: __internal_L2:
seq r2 r8 2 seq r2 r8 2
beq r2 0 L4 beqz r2 __internal_L4
move r8 20 #a move r8 20
j L3 j __internal_L3
L4: __internal_L4:
move r8 30 #a move r8 30
L3: __internal_L3:
L1: __internal_L1:
" "
} }
); );
@@ -112,8 +129,8 @@ fn test_if_else_if_statement() -> anyhow::Result<()> {
#[test] #[test]
fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> { fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let a = 1; let a = 1;
let b = 2; 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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 1 #a move r8 1
move r9 2 #b move r9 2
move r10 3 #c move r10 3
move r11 4 #d move r11 4
move r12 5 #e move r12 5
move r13 6 #f move r13 6
move r14 7 #g move r14 7
push 8 #h push 8
seq r1 r8 1 seq r1 r8 1
beq r1 0 L1 beqz r1 __internal_L1
sub r0 sp 1 sub r0 sp 1
put db r0 99 #h put db r0 99
L1: __internal_L1:
sub sp sp 1 sub sp sp 1
" "
} }

View File

@@ -1,42 +1,47 @@
use crate::compile;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn no_arguments() -> anyhow::Result<()> { fn no_arguments() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
fn doSomething() {}; fn doSomething() {};
let i = doSomething(); let i = doSomething();
" "
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
let to_test = indoc! { let to_test = indoc! {
" "
j main j main
doSomething: doSomething:
push sp
push ra push ra
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
main: main:
jal doSomething jal doSomething
move r8 r15 #i move r8 r15
" "
}; };
assert_eq!(compiled, to_test); assert_eq!(result.output, to_test);
Ok(()) Ok(())
} }
#[test] #[test]
fn let_var_args() -> anyhow::Result<()> { fn let_var_args() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
fn mul2(arg1) { fn mul2(arg1) {
return arg1 * 2; return arg1 * 2;
@@ -49,36 +54,40 @@ fn let_var_args() -> anyhow::Result<()> {
" "
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
mul2: mul2:
pop r8 #arg1 pop r8
push sp
push ra push ra
mul r1 r8 2 mul r1 r8 2
move r15 r1 move r15 r1
j L1 j __internal_L1
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
main: main:
L2: __internal_L2:
move r8 123 #arg1 move r8 123
push r8 push r8
push r8 push r8
jal mul2 jal mul2
sub r0 sp 1 pop r8
get r8 db r0 move r9 r15
sub sp sp 1
move r9 r15 #i
pow r1 r9 2 pow r1 r9 2
move r9 r1 #i move r9 r1
j L2 j __internal_L2
L3: __internal_L3:
" "
} }
); );
@@ -106,8 +115,8 @@ fn incorrect_args_count() -> anyhow::Result<()> {
#[test] #[test]
fn inline_literal_args() -> anyhow::Result<()> { fn inline_literal_args() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
fn doSomething(arg1, arg2) { fn doSomething(arg1, arg2) {
return 5; return 5;
@@ -117,32 +126,36 @@ fn inline_literal_args() -> anyhow::Result<()> {
" "
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
doSomething: doSomething:
pop r8 #arg2 pop r8
pop r9 #arg1 pop r9
push sp
push ra push ra
move r15 5 #returnValue move r15 5
j L1 j __internal_L1
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
main: main:
move r8 123 #thisVariableShouldStayInPlace move r8 123
push r8 push r8
push 12 push 12
push 34 push 34
jal doSomething jal doSomething
sub r0 sp 1 pop r8
get r8 db r0 move r9 r15
sub sp sp 1
move r9 r15 #returnedValue
" "
} }
); );
@@ -152,8 +165,8 @@ fn inline_literal_args() -> anyhow::Result<()> {
#[test] #[test]
fn mixed_args() -> anyhow::Result<()> { fn mixed_args() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let arg1 = 123; let arg1 = 123;
let returnValue = doSomething(arg1, 456); let returnValue = doSomething(arg1, 456);
@@ -161,30 +174,34 @@ fn mixed_args() -> anyhow::Result<()> {
" "
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
doSomething: doSomething:
pop r8 #arg2 pop r8
pop r9 #arg1 pop r9
push sp
push ra push ra
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
main: main:
move r8 123 #arg1 move r8 123
push r8 push r8
push r8 push r8
push 456 push 456
jal doSomething jal doSomething
sub r0 sp 1 pop r8
get r8 db r0 move r9 r15
sub sp sp 1
move r9 r15 #returnValue
" "
} }
); );
@@ -194,8 +211,8 @@ fn mixed_args() -> anyhow::Result<()> {
#[test] #[test]
fn with_return_statement() -> anyhow::Result<()> { fn with_return_statement() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
fn doSomething(arg1) { fn doSomething(arg1) {
return 456; return 456;
@@ -205,25 +222,31 @@ fn with_return_statement() -> anyhow::Result<()> {
" "
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
doSomething: doSomething:
pop r8 #arg1 pop r8
push sp
push ra push ra
move r15 456 #returnValue move r15 456
j L1 j __internal_L1
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
main: main:
push 123 push 123
jal doSomething jal doSomething
move r8 r15 #returned move r8 r15
" "
} }
); );
@@ -233,8 +256,8 @@ fn with_return_statement() -> anyhow::Result<()> {
#[test] #[test]
fn with_negative_return_literal() -> anyhow::Result<()> { fn with_negative_return_literal() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
fn doSomething() { fn doSomething() {
return -1; return -1;
@@ -243,22 +266,28 @@ fn with_negative_return_literal() -> anyhow::Result<()> {
" "
}; };
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
doSomething: doSomething:
push sp
push ra push ra
move r15 -1 #returnValue move r15 -1
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
main: main:
jal doSomething jal doSomething
move r8 r15 #i move r8 r15
" "
} }
); );

View File

@@ -4,18 +4,24 @@ use pretty_assertions::assert_eq;
#[test] #[test]
fn variable_declaration_numeric_literal() -> anyhow::Result<()> { fn variable_declaration_numeric_literal() -> anyhow::Result<()> {
let compiled = crate::compile! { let compiled = crate::compile! {
debug r#" check r#"
let i = 20c; let i = 20c;
"# "#
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 293.15 #i move r8 293.15
" "
} }
); );
@@ -26,7 +32,7 @@ fn variable_declaration_numeric_literal() -> anyhow::Result<()> {
#[test] #[test]
fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()> { fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
let a = 0; let a = 0;
let b = 1; let b = 1;
@@ -40,22 +46,28 @@ fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()>
let j = 9; let j = 9;
"#}; "#};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 0 #a move r8 0
move r9 1 #b move r9 1
move r10 2 #c move r10 2
move r11 3 #d move r11 3
move r12 4 #e move r12 4
move r13 5 #f move r13 5
move r14 6 #g move r14 6
push 7 #h push 7
push 8 #i push 8
push 9 #j push 9
sub sp sp 3 sub sp sp 3
" "
} }
@@ -67,19 +79,25 @@ fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()>
#[test] #[test]
fn variable_declaration_negative() -> anyhow::Result<()> { fn variable_declaration_negative() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = -1; let i = -1;
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 -1 #i move r8 -1
" "
} }
); );
@@ -90,21 +108,27 @@ fn variable_declaration_negative() -> anyhow::Result<()> {
#[test] #[test]
fn test_boolean_declaration() -> anyhow::Result<()> { fn test_boolean_declaration() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let t = true; let t = true;
let f = false; let f = false;
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 1 #t move r8 1
move r9 0 #f move r9 0
" "
} }
); );
@@ -115,7 +139,7 @@ fn test_boolean_declaration() -> anyhow::Result<()> {
#[test] #[test]
fn test_boolean_return() -> anyhow::Result<()> { fn test_boolean_return() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
fn getTrue() { fn getTrue() {
return true; return true;
@@ -125,23 +149,29 @@ fn test_boolean_return() -> anyhow::Result<()> {
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
getTrue: getTrue:
push sp
push ra push ra
move r15 1 #returnValue move r15 1
j L1 j __internal_L1
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
main: main:
jal getTrue jal getTrue
move r8 r15 #val move r8 r15
" "
} }
); );
@@ -151,15 +181,21 @@ fn test_boolean_return() -> anyhow::Result<()> {
#[test] #[test]
fn test_const_hash_expr() -> anyhow::Result<()> { fn test_const_hash_expr() -> anyhow::Result<()> {
let compiled = compile!(debug r#" let compiled = compile!(check r#"
const nameHash = hash("AccessCard"); const nameHash = hash("AccessCard");
device self = "db"; device self = "db";
self.Setting = nameHash; self.Setting = nameHash;
"#); "#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
@@ -170,3 +206,34 @@ fn test_const_hash_expr() -> anyhow::Result<()> {
); );
Ok(()) 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,29 +3,69 @@ use pretty_assertions::assert_eq;
#[test] #[test]
fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> { 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 // 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!( assert_eq!(
compiled, compiled.output,
indoc! {" indoc! {"
j main j main
doSomething: doSomething:
pop r8 #arg9 pop r8
pop r9 #arg8 pop r9
pop r10 #arg7 pop r10
pop r11 #arg6 pop r11
pop r12 #arg5 pop r12
pop r13 #arg4 pop r13
pop r14 #arg3 pop r14
push sp
push ra push ra
L1: sub r0 sp 4
sub r0 sp 1 get r1 db r0
get ra db r0 sub r0 sp 3
sub sp 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 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
"} "}
); );
@@ -34,7 +74,7 @@ fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
#[test] #[test]
fn test_early_return() -> anyhow::Result<()> { fn test_early_return() -> anyhow::Result<()> {
let compiled = compile!(debug r#" let compiled = compile!(check r#"
// This is a test function declaration with no body // This is a test function declaration with no body
fn doSomething() { fn doSomething() {
if (1 == 1) { if (1 == 1) {
@@ -46,27 +86,33 @@ fn test_early_return() -> anyhow::Result<()> {
doSomething(); doSomething();
"#); "#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
doSomething: doSomething:
push sp
push ra push ra
seq r1 1 1 seq r1 1 1
beq r1 0 L2 beqz r1 __internal_L2
j L1 j __internal_L1
L2: __internal_L2:
move r8 3 #i move r8 3
j L1 j __internal_L1
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
main: main:
jal doSomething jal doSomething
move r1 r15 #__binary_temp_2 move r1 r15
" "
} }
); );
@@ -76,24 +122,30 @@ fn test_early_return() -> anyhow::Result<()> {
#[test] #[test]
fn test_function_declaration_with_register_params() -> anyhow::Result<()> { fn test_function_declaration_with_register_params() -> anyhow::Result<()> {
let compiled = compile!(debug r#" let compiled = compile!(check r#"
// This is a test function declaration with no body // This is a test function declaration with no body
fn doSomething(arg1, arg2) { fn doSomething(arg1, arg2) {
}; };
"#); "#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! {" indoc! {"
j main j main
doSomething: doSomething:
pop r8 #arg2 pop r8
pop r9 #arg1 pop r9
push sp
push ra push ra
L1: __internal_L1:
sub r0 sp 1 pop ra
get ra db r0 pop sp
sub sp sp 1
j ra j ra
"} "}
); );

View File

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

View File

@@ -1,11 +1,10 @@
use crate::compile;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn test_infinite_loop() -> anyhow::Result<()> { fn test_infinite_loop() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let a = 0; let a = 0;
loop { 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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 0 #a move r8 0
L1: __internal_L1:
add r1 r8 1 add r1 r8 1
move r8 r1 #a move r8 r1
j L1 j __internal_L1
L2: __internal_L2:
" "
} }
); );
@@ -36,8 +41,8 @@ fn test_infinite_loop() -> anyhow::Result<()> {
#[test] #[test]
fn test_loop_break() -> anyhow::Result<()> { fn test_loop_break() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let a = 0; let a = 0;
loop { 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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 0 #a move r8 0
L1: __internal_L1:
add r1 r8 1 add r1 r8 1
move r8 r1 #a move r8 r1
sgt r2 r8 10 sgt r2 r8 10
beq r2 0 L3 beqz r2 __internal_L3
j L2 j __internal_L2
L3: __internal_L3:
j L1 j __internal_L1
L2: __internal_L2:
" "
} }
); );
@@ -75,8 +86,8 @@ fn test_loop_break() -> anyhow::Result<()> {
#[test] #[test]
fn test_while_loop() -> anyhow::Result<()> { fn test_while_loop() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
" "
let a = 0; let a = 0;
while (a < 10) { 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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 0 #a move r8 0
L1: __internal_L1:
slt r1 r8 10 slt r1 r8 10
beq r1 0 L2 beqz r1 __internal_L2
add r2 r8 1 add r2 r8 1
move r8 r2 #a move r8 r2
j L1 j __internal_L1
L2: __internal_L2:
" "
} }
); );
@@ -109,8 +126,8 @@ fn test_while_loop() -> anyhow::Result<()> {
#[test] #[test]
fn test_loop_continue() -> anyhow::Result<()> { fn test_loop_continue() -> anyhow::Result<()> {
let compiled = compile! { let result = compile! {
debug check
r#" r#"
let a = 0; let a = 0;
loop { 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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 0 #a move r8 0
L1: __internal_L1:
add r1 r8 1 add r1 r8 1
move r8 r1 #a move r8 r1
slt r2 r8 5 slt r2 r8 5
beq r2 0 L3 beqz r2 __internal_L3
j L1 j __internal_L1
L3: __internal_L3:
j L2 j __internal_L2
j L1 j __internal_L1
L2: __internal_L2:
" "
} }
); );

View File

@@ -1,4 +1,3 @@
use crate::compile;
use anyhow::Result; use anyhow::Result;
use indoc::indoc; use indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -6,20 +5,26 @@ use pretty_assertions::assert_eq;
#[test] #[test]
fn test_acos() -> Result<()> { fn test_acos() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = acos(123); let i = acos(123);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
acos r15 123 acos r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -30,20 +35,26 @@ fn test_acos() -> Result<()> {
#[test] #[test]
fn test_asin() -> Result<()> { fn test_asin() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = asin(123); let i = asin(123);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
asin r15 123 asin r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -54,20 +65,26 @@ fn test_asin() -> Result<()> {
#[test] #[test]
fn test_atan() -> Result<()> { fn test_atan() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = atan(123); let i = atan(123);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
atan r15 123 atan r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -78,20 +95,26 @@ fn test_atan() -> Result<()> {
#[test] #[test]
fn test_atan2() -> Result<()> { fn test_atan2() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = atan2(123, 456); let i = atan2(123, 456);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
atan2 r15 123 456 atan2 r15 123 456
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -102,20 +125,26 @@ fn test_atan2() -> Result<()> {
#[test] #[test]
fn test_abs() -> Result<()> { fn test_abs() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = abs(-123); let i = abs(-123);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
abs r15 -123 abs r15 -123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -126,20 +155,26 @@ fn test_abs() -> Result<()> {
#[test] #[test]
fn test_ceil() -> Result<()> { fn test_ceil() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = ceil(123.90); let i = ceil(123.90);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
ceil r15 123.90 ceil r15 123.90
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -150,20 +185,26 @@ fn test_ceil() -> Result<()> {
#[test] #[test]
fn test_cos() -> Result<()> { fn test_cos() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = cos(123); let i = cos(123);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
cos r15 123 cos r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -174,20 +215,26 @@ fn test_cos() -> Result<()> {
#[test] #[test]
fn test_floor() -> Result<()> { fn test_floor() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = floor(123); let i = floor(123);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
floor r15 123 floor r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -198,20 +245,26 @@ fn test_floor() -> Result<()> {
#[test] #[test]
fn test_log() -> Result<()> { fn test_log() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = log(123); let i = log(123);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
log r15 123 log r15 123
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -222,20 +275,26 @@ fn test_log() -> Result<()> {
#[test] #[test]
fn test_max() -> Result<()> { fn test_max() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = max(123, 456); let i = max(123, 456);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
max r15 123 456 max r15 123 456
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -246,22 +305,28 @@ fn test_max() -> Result<()> {
#[test] #[test]
fn test_max_from_game() -> Result<()> { fn test_max_from_game() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
let item = 0; let item = 0;
item = max(1 + 2, 2); item = max(1 + 2, 2);
"# "#
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 0 #item move r8 0
max r15 3 2 max r15 3 2
move r8 r15 #item move r8 r15
" "
} }
); );
@@ -272,20 +337,26 @@ fn test_max_from_game() -> Result<()> {
#[test] #[test]
fn test_min() -> Result<()> { fn test_min() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = min(123, 456); let i = min(123, 456);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
min r15 123 456 min r15 123 456
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -296,20 +367,26 @@ fn test_min() -> Result<()> {
#[test] #[test]
fn test_rand() -> Result<()> { fn test_rand() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = rand(); let i = rand();
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
rand r15 rand r15
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -320,20 +397,26 @@ fn test_rand() -> Result<()> {
#[test] #[test]
fn test_sin() -> Result<()> { fn test_sin() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = sin(3); let i = sin(3);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
sin r15 3 sin r15 3
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -344,20 +427,26 @@ fn test_sin() -> Result<()> {
#[test] #[test]
fn test_sqrt() -> Result<()> { fn test_sqrt() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = sqrt(3); let i = sqrt(3);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
sqrt r15 3 sqrt r15 3
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -368,20 +457,26 @@ fn test_sqrt() -> Result<()> {
#[test] #[test]
fn test_tan() -> Result<()> { fn test_tan() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = tan(3); let i = tan(3);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
tan r15 3 tan r15 3
move r8 r15 #i move r8 r15
" "
} }
); );
@@ -392,20 +487,26 @@ fn test_tan() -> Result<()> {
#[test] #[test]
fn test_trunc() -> Result<()> { fn test_trunc() -> Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
let i = trunc(3.234); let i = trunc(3.234);
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
trunc r15 3.234 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)] #[cfg_attr(test, macro_export)]
macro_rules! compile { macro_rules! compile {
($source:expr) => {{ ($source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new()); let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = ::Compiler::new( let compiler = ::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))), parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))),
&mut writer,
None, None,
); );
compiler.compile(); let res = compiler.compile();
res.instructions.write(&mut writer)?;
output!(writer) output!(writer)
}}; }};
(result $source:expr) => {{ (result $source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = crate::Compiler::new( let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from($source)), parser::Parser::new(tokenizer::Tokenizer::from($source)),
&mut writer,
Some(crate::CompilerConfig { debug: true }), Some(crate::CompilerConfig { debug: true }),
); );
compiler.compile() compiler.compile().errors
}}; }};
(debug $source:expr) => {{ (check $source:expr) => {{
let mut writer = std::io::BufWriter::new(Vec::new()); let mut writer = std::io::BufWriter::new(Vec::new());
let compiler = crate::Compiler::new( let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from($source)), parser::Parser::new(tokenizer::Tokenizer::from($source)),
&mut writer,
Some(crate::CompilerConfig { debug: true }), Some(crate::CompilerConfig { debug: true }),
); );
compiler.compile(); let res = compiler.compile();
output!(writer) res.instructions.write(&mut writer)?;
let output = output!(writer);
crate::test::CompilationCheckResult {
errors: res.errors,
output,
}
}}; }};
} }
mod binary_expression; mod binary_expression;
mod branching; mod branching;
mod declaration_function_invocation; mod declaration_function_invocation;
mod declaration_literal; mod declaration_literal;
mod device_access;
mod edge_cases;
mod error_handling;
mod function_declaration; mod function_declaration;
mod logic_expression; mod logic_expression;
mod loops; mod loops;
mod math_syscall; mod math_syscall;
mod negation_priority;
mod scoping;
mod syscall; 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 indoc::indoc;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn test_yield() -> anyhow::Result<()> { fn test_yield() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
yield(); yield();
" "
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
@@ -28,7 +33,7 @@ fn test_yield() -> anyhow::Result<()> {
#[test] #[test]
fn test_sleep() -> anyhow::Result<()> { fn test_sleep() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
" "
sleep(3); sleep(3);
let sleepAmount = 15; 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!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
sleep 3 sleep 3
move r8 15 #sleepAmount move r8 15
sleep r8 sleep r8
mul r1 r8 2 mul r1 r8 2
sleep r1 sleep r1
@@ -58,7 +69,7 @@ fn test_sleep() -> anyhow::Result<()> {
#[test] #[test]
fn test_set_on_device() -> anyhow::Result<()> { fn test_set_on_device() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
device airConditioner = "d0"; device airConditioner = "d0";
let internalTemp = 20c; 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!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
move r8 293.15 #internalTemp move r8 293.15
sgt r1 r8 298.15 sgt r1 r8 298.15
s d0 On r1 s d0 On r1
" "
@@ -86,15 +103,21 @@ fn test_set_on_device() -> anyhow::Result<()> {
#[test] #[test]
fn test_set_on_device_batched() -> anyhow::Result<()> { fn test_set_on_device_batched() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
const doorHash = hash("Door"); const doorHash = hash("Door");
setBatched(doorHash, "Lock", true); setBatched(doorHash, "Lock", true);
"# "#
}; };
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
r#" r#"
j main j main
@@ -109,7 +132,7 @@ fn test_set_on_device_batched() -> anyhow::Result<()> {
#[test] #[test]
fn test_set_on_device_batched_named() -> anyhow::Result<()> { fn test_set_on_device_batched_named() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
device dev = "d0"; device dev = "d0";
const devName = hash("test"); 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!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
@@ -135,7 +164,7 @@ fn test_set_on_device_batched_named() -> anyhow::Result<()> {
#[test] #[test]
fn test_load_from_device() -> anyhow::Result<()> { fn test_load_from_device() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
device airCon = "d0"; 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!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
l r15 d0 On l r15 d0 On
move r8 r15 #setting move r8 r15
" "
} }
); );
@@ -161,7 +196,7 @@ fn test_load_from_device() -> anyhow::Result<()> {
#[test] #[test]
fn test_load_from_slot() -> anyhow::Result<()> { fn test_load_from_slot() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
device airCon = "d0"; 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!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
main: main:
ls r15 d0 0 Occupied ls r15 d0 0 Occupied
move r8 r15 #setting move r8 r15
" "
} }
); );
@@ -187,7 +228,7 @@ fn test_load_from_slot() -> anyhow::Result<()> {
#[test] #[test]
fn test_set_slot() -> anyhow::Result<()> { fn test_set_slot() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
device airCon = "d0"; 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!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
@@ -208,3 +255,35 @@ fn test_set_slot() -> anyhow::Result<()> {
Ok(()) 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 // r1 - r7 : Temporary Variables
// r8 - r14 : Persistant Variables // r8 - r14 : Persistant Variables
use helpers::Span;
use lsp_types::{Diagnostic, DiagnosticSeverity}; use lsp_types::{Diagnostic, DiagnosticSeverity};
use parser::tree_node::{Literal, Span}; use parser::tree_node::Literal;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},

View File

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

View File

@@ -1,4 +1,5 @@
use crc32fast::hash as crc32_hash; use crc32fast::hash as crc32_hash;
/// This function takes an input which is meant to be hashed via the CRC32 algorithm, but it then /// This function takes an input which is meant to be hashed via the CRC32 algorithm, but it then
/// converts the generated UNSIGNED number into it's SIGNED counterpart. /// converts the generated UNSIGNED number into it's SIGNED counterpart.
pub fn crc_hash_signed(input: &str) -> i128 { pub fn crc_hash_signed(input: &str) -> i128 {
@@ -9,3 +10,38 @@ pub fn crc_hash_signed(input: &str) -> i128 {
hash_value_i32 as i128 hash_value_i32 as i128
} }
/// Removes common leading whitespace from all lines in a string (dedent).
/// This is useful for cleaning up documentation strings that have uniform indentation.
pub fn dedent(text: &str) -> String {
let lines: Vec<&str> = text.lines().collect();
// Find minimum indentation (excluding empty lines)
let mut min_indent = usize::MAX;
for line in &lines {
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
min_indent = min_indent.min(indent);
}
}
// If no lines or all empty, return as-is
if min_indent == usize::MAX {
return text.to_string();
}
// Remove the common indentation
lines
.iter()
.map(|line| {
if line.trim().is_empty() {
""
} else if line.len() >= min_indent {
&line[min_indent..]
} else {
line
}
})
.collect::<Vec<_>>()
.join("\n")
}

View File

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

View File

@@ -99,12 +99,12 @@ macro_rules! documented {
),* ),*
]; ];
doc_lines.iter() let combined = doc_lines.iter()
.filter_map(|&d| d) .filter_map(|&d| d)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n") .join("\n");
.trim()
.to_string() $crate::dedent(&combined).trim().to_string()
} }
)* )*
} }
@@ -122,12 +122,13 @@ macro_rules! documented {
documented!(@doc_filter #[ $($variant_attr)* ]) documented!(@doc_filter #[ $($variant_attr)* ])
),* ),*
]; ];
doc_lines.iter()
let combined = doc_lines.iter()
.filter_map(|&d| d) .filter_map(|&d| d)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n") .join("\n");
.trim()
.to_string() $crate::dedent(&combined).trim().to_string()
} }
) )
),* ),*
@@ -136,4 +137,3 @@ macro_rules! documented {
} }
}; };
} }

View File

@@ -10,6 +10,7 @@ macro_rules! with_syscalls {
"loadBatched", "loadBatched",
"loadBatchedNamed", "loadBatchedNamed",
"loadSlot", "loadSlot",
"loadReagent",
"set", "set",
"setBatched", "setBatched",
"setBatchedNamed", "setBatchedNamed",
@@ -35,6 +36,7 @@ macro_rules! with_syscalls {
"lb", "lb",
"lbn", "lbn",
"ls", "ls",
"lr",
"s", "s",
"sb", "sb",
"sbn", "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,357 @@
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(())
}
}
#[derive(Clone)]
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,2 @@
# Treat snapshot files as text
*.snap text

View File

@@ -0,0 +1,19 @@
[package]
name = "integration_tests"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
compiler = { path = "../compiler" }
parser = { path = "../parser" }
tokenizer = { path = "../tokenizer" }
optimizer = { path = "../optimizer" }
il = { path = "../il" }
anyhow = { workspace = true }
indoc = "2"
insta = "1.40"
[lib]
# This is a test-only crate
path = "src/lib.rs"

View File

@@ -0,0 +1,92 @@
# Integration Tests for Slang Compiler with Optimizer
This crate contains end-to-end integration tests for the Slang compiler that verify the complete compilation pipeline including all optimization passes.
## Snapshot Testing with Insta
These tests use [insta](https://insta.rs/) for snapshot testing, which captures the entire compiled output and stores it in snapshot files for comparison.
### Running Tests
```bash
# Run all integration tests
cargo test --package integration_tests
# Run a specific test
cargo test --package integration_tests test_simple_leaf_function
```
### Updating Snapshots
When you make changes to the compiler or optimizer that affect the output:
```bash
# Update all snapshots automatically
INSTA_UPDATE=always cargo test --package integration_tests
# Or use cargo-insta for interactive review (install first: cargo install cargo-insta)
cargo insta test --package integration_tests
cargo insta review --package integration_tests
```
### Understanding Snapshots
Snapshot files are stored in `src/snapshots/` and contain:
- The full IC10 assembly output from compiling Slang source code
- Metadata about which test generated them
- The expression that produced the output
Example snapshot structure:
```
---
source: libs/integration_tests/src/lib.rs
expression: output
---
j main
move r8 10
j ra
```
### What We Test
1. **Leaf Function Optimization** - Removal of unnecessary `push sp/ra` and `pop ra/sp`
2. **Function Calls** - Preservation of stack frame when calling functions
3. **Constant Folding** - Compile-time evaluation of constant expressions
4. **Algebraic Simplification** - Identity operations like `x * 1``x`
5. **Strength Reduction** - Converting expensive operations like `x * 2``x + x`
6. **Dead Code Elimination** - Removal of unused variables
7. **Peephole Comparison Fusion** - Combining comparison + branch instructions
8. **Select Optimization** - Converting if/else to single `select` instruction
9. **Complex Arithmetic** - Multiple optimizations working together
10. **Nested Function Calls** - Full program optimization
### Adding New Tests
To add a new integration test:
1. Add a new `#[test]` function in `src/lib.rs`
2. Call `compile_optimized()` with your Slang source code
3. Use `insta::assert_snapshot!(output)` to capture the output
4. Run with `INSTA_UPDATE=always` to create the initial snapshot
5. Review the snapshot file to ensure it looks correct
Example:
```rust
#[test]
fn test_my_optimization() {
let source = "fn foo(x) { return x + 1; }";
let output = compile_optimized(source);
insta::assert_snapshot!(output);
}
```
### Benefits of Snapshot Testing
- **Full Output Verification**: Tests the entire compiled output, not just snippets
- **Easy to Review**: Visual diffs show exactly what changed in the output
- **Regression Detection**: Any change to output is immediately visible
- **Living Documentation**: Snapshots serve as examples of compiler output
- **Less Brittle**: No need to manually update expected strings when making intentional changes

View File

@@ -0,0 +1,244 @@
//! Integration tests for the Slang compiler with optimizer
//!
//! These tests compile Slang source code and verify both the compilation
//! and optimization passes work correctly together using snapshot testing.
#[cfg(test)]
mod tests {
use compiler::Compiler;
use indoc::indoc;
use parser::Parser;
use tokenizer::Tokenizer;
/// Compile Slang source code and return both unoptimized and optimized output
fn compile_with_and_without_optimization(source: &str) -> String {
// Compile for unoptimized output
let tokenizer = Tokenizer::from(source);
let parser = Parser::new(tokenizer);
let compiler = Compiler::new(parser, None);
let result = compiler.compile();
assert!(
result.errors.is_empty(),
"Compilation errors: {:?}",
result.errors
);
// Get unoptimized output
let mut unoptimized_writer = std::io::BufWriter::new(Vec::new());
result
.instructions
.write(&mut unoptimized_writer)
.expect("Failed to write unoptimized output");
let unoptimized_bytes = unoptimized_writer
.into_inner()
.expect("Failed to get bytes");
let unoptimized = String::from_utf8(unoptimized_bytes).expect("Invalid UTF-8");
// Compile again for optimized output
let tokenizer2 = Tokenizer::from(source);
let parser2 = Parser::new(tokenizer2);
let compiler2 = Compiler::new(parser2, None);
let result2 = compiler2.compile();
// Apply optimizations
let optimized_instructions = optimizer::optimize(result2.instructions);
// Get optimized output
let mut optimized_writer = std::io::BufWriter::new(Vec::new());
optimized_instructions
.write(&mut optimized_writer)
.expect("Failed to write optimized output");
let optimized_bytes = optimized_writer.into_inner().expect("Failed to get bytes");
let optimized = String::from_utf8(optimized_bytes).expect("Invalid UTF-8");
// Combine both outputs with clear separators
format!(
"## Unoptimized Output\n\n{}\n## Optimized Output\n\n{}",
unoptimized, optimized
)
}
#[test]
fn test_simple_leaf_function() {
let source = "fn test() { let x = 10; }";
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_function_with_call() {
let source = indoc! {"
fn add(a, b) { return a + b; }
fn main() { let x = add(5, 10); }
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_constant_folding() {
let source = "let x = 5 + 10;";
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_algebraic_simplification() {
let source = "let x = 5; let y = x * 1;";
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_strength_reduction() {
let source = "fn double(x) { return x * 2; }";
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_dead_code_elimination() {
let source = indoc! {"
fn compute(x) {
let unused = 20;
return x + 1;
}
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_peephole_comparison_fusion() {
let source = indoc! {"
fn compare(x, y) {
if (x > y) {
let z = 1;
}
}
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_select_optimization() {
let source = indoc! {"
fn ternary(cond) {
let result = 0;
if (cond) {
result = 10;
} else {
result = 20;
}
return result;
}
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_leaf_function_no_stack_frame() {
let source = indoc! {"
fn increment(x) {
x = x + 1;
}
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_complex_arithmetic() {
let source = indoc! {"
fn compute(a, b, c) {
let x = a * 2;
let y = b + 0;
let z = c * 1;
return x + y + z;
}
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_nested_function_calls() {
let source = indoc! {"
fn add(a, b) { return a + b; }
fn multiply(x, y) { return x * 2; }
fn complex(a, b) {
let sum = add(a, b);
let doubled = multiply(sum, 2);
return doubled;
}
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_tuples() {
let source = indoc! {r#"
device self = "db";
device day = "d0";
fn getSomethingElse(input) {
return input + 1;
}
fn getSensorData() {
return (
day.Vertical,
day.Horizontal,
getSomethingElse(3)
);
}
loop {
yield();
let (vertical, horizontal, _) = getSensorData();
(horizontal, _, _) = getSensorData();
self.Setting = horizontal;
}
"#};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_larre_script() {
let source = include_str!("./test_files/test_larre_script.stlg");
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_reagent_processing() {
let source = include_str!("./test_files/reagent_processing.stlg");
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_setbatched_with_member_access() {
let source = indoc! {r#"
const SENSOR = 20088;
const PANELS = hash("StructureSolarPanelDual");
loop {
setBatched(PANELS, "Horizontal", SENSOR.Horizontal);
setBatched(PANELS, "Vertical", SENSOR.Vertical + 90);
yield();
}
"#};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
}

View File

@@ -0,0 +1,17 @@
---
source: libs/integration_tests/src/lib.rs
expression: output
---
## Unoptimized Output
j main
main:
move r8 5
mul r1 r8 1
move r9 r1
## Optimized Output
move r8 5
move r1 5
move r9 5

View File

@@ -0,0 +1,45 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 158
expression: output
---
## Unoptimized Output
j main
compute:
pop r8
pop r9
pop r10
push sp
push ra
mul r1 r10 2
move r11 r1
add r2 r9 0
move r12 r2
mul r3 r8 1
move r13 r3
add r4 r11 r12
add r5 r4 r13
move r15 r5
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
## Optimized Output
j main
pop r8
pop r9
pop r10
push sp
push ra
add r11 r10 r10
move r12 r9
move r13 r8
add r4 r11 r12
add r15 r4 r13
pop ra
pop sp
j ra

View File

@@ -0,0 +1,13 @@
---
source: libs/integration_tests/src/lib.rs
expression: output
---
## Unoptimized Output
j main
main:
move r8 15
## Optimized Output
move r8 15

View File

@@ -0,0 +1,32 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 103
expression: output
---
## Unoptimized Output
j main
compute:
pop r8
push sp
push ra
move r9 20
add r1 r8 1
move r15 r1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
## Optimized Output
j main
pop r8
push sp
push ra
move r9 20
add r15 r8 1
pop ra
pop sp
j ra

View File

@@ -0,0 +1,52 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 70
expression: output
---
## Unoptimized Output
j main
add:
pop r8
pop r9
push sp
push ra
add r1 r9 r8
move r15 r1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
push sp
push ra
push 5
push 10
jal add
move r8 r15
__internal_L2:
pop ra
pop sp
j ra
## Optimized Output
j 9
pop r8
pop r9
push sp
push ra
add r15 r9 r8
pop ra
pop sp
j ra
push sp
push ra
push 5
push 10
jal 1
move r8 r15
pop ra
pop sp
j ra

View File

@@ -0,0 +1,223 @@
---
source: libs/integration_tests/src/lib.rs
expression: output
---
## Unoptimized Output
j main
waitForIdle:
push sp
push ra
yield
__internal_L2:
l r1 d0 Idle
seq r2 r1 0
beqz r2 __internal_L3
yield
j __internal_L2
__internal_L3:
__internal_L1:
pop ra
pop sp
j ra
deposit:
push sp
push ra
s d0 Setting 1
jal waitForIdle
move r1 r15
s d0 Activate 1
jal waitForIdle
move r2 r15
s d1 Open 0
__internal_L4:
pop ra
pop sp
j ra
checkAndHarvest:
pop r8
push sp
push ra
sle r1 r8 1
ls r15 d0 255 Seeding
slt r2 r15 1
or r3 r1 r2
beqz r3 __internal_L6
j __internal_L5
__internal_L6:
__internal_L7:
ls r15 d0 255 Mature
beqz r15 __internal_L8
yield
s d0 Activate 1
j __internal_L7
__internal_L8:
ls r15 d0 255 Occupied
move r9 r15
s d0 Setting 1
push r8
push r9
jal waitForIdle
pop r9
pop r8
move r4 r15
push r8
push r9
jal deposit
pop r9
pop r8
move r5 r15
beqz r9 __internal_L9
push r8
push r9
jal deposit
pop r9
pop r8
move r6 r15
__internal_L9:
s d0 Setting r8
push r8
push r9
jal waitForIdle
pop r9
pop r8
move r6 r15
ls r15 d0 0 Occupied
beqz r15 __internal_L10
s d0 Activate 1
__internal_L10:
push r8
push r9
jal waitForIdle
pop r9
pop r8
move r7 r15
__internal_L5:
pop ra
pop sp
j ra
main:
move r8 0
__internal_L11:
yield
l r1 d0 Idle
seq r2 r1 0
beqz r2 __internal_L13
j __internal_L11
__internal_L13:
add r3 r8 1
sgt r4 r3 19
add r5 r8 1
select r6 r4 2 r5
move r9 r6
push r8
push r9
push r8
jal checkAndHarvest
pop r9
pop r8
move r7 r15
s d0 Setting r9
move r8 r9
j __internal_L11
__internal_L12:
## Optimized Output
j 77
push sp
push ra
yield
l r1 d0 Idle
bne r1 0 8
yield
j 4
pop ra
pop sp
j ra
push sp
push ra
s d0 Setting 1
jal 1
move r1 r15
s d0 Activate 1
jal 1
move r2 r15
s d1 Open 0
pop ra
pop sp
j ra
pop r8
push sp
push ra
sle r1 r8 1
ls r15 d0 255 Seeding
slt r2 r15 1
or r3 r1 r2
beqz r3 32
j 74
ls r15 d0 255 Mature
beqz r15 37
yield
s d0 Activate 1
j 32
ls r9 d0 255 Occupied
s d0 Setting 1
push r8
push r9
jal 1
pop r9
pop r8
move r4 r15
push r8
push r9
jal 11
pop r9
pop r8
move r5 r15
beqz r9 58
push r8
push r9
jal 11
pop r9
pop r8
move r6 r15
s d0 Setting r8
push r8
push r9
jal 1
pop r9
pop r8
move r6 r15
ls r15 d0 0 Occupied
beqz r15 68
s d0 Activate 1
push r8
push r9
jal 1
pop r9
pop r8
move r7 r15
pop ra
pop sp
j ra
move r8 0
yield
l r1 d0 Idle
bne r1 0 82
j 78
add r3 r8 1
sgt r4 r3 19
add r5 r8 1
select r6 r4 2 r5
move r9 r6
push r8
push r9
push r8
jal 23
pop r9
pop r8
move r7 r15
s d0 Setting r9
move r8 r9
j 78

View File

@@ -0,0 +1,30 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 144
expression: output
---
## Unoptimized Output
j main
increment:
pop r8
push sp
push ra
add r1 r8 1
move r8 r1
__internal_L1:
pop ra
pop sp
j ra
## Optimized Output
j main
pop r8
push sp
push ra
add r1 r8 1
move r8 r1
pop ra
pop sp
j ra

View File

@@ -0,0 +1,107 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 173
expression: output
---
## Unoptimized Output
j main
add:
pop r8
pop r9
push sp
push ra
add r1 r9 r8
move r15 r1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
multiply:
pop r8
pop r9
push sp
push ra
mul r1 r9 2
move r15 r1
j __internal_L2
__internal_L2:
pop ra
pop sp
j ra
complex:
pop r8
pop r9
push sp
push ra
push r8
push r9
push r9
push r8
jal add
pop r9
pop r8
move r10 r15
push r8
push r9
push r10
push r10
push 2
jal multiply
pop r10
pop r9
pop r8
move r11 r15
move r15 r11
j __internal_L3
__internal_L3:
pop ra
pop sp
j ra
## Optimized Output
j main
pop r8
pop r9
push sp
push ra
add r15 r9 r8
pop ra
pop sp
j ra
pop r8
pop r9
push sp
push ra
add r15 r9 r9
pop ra
pop sp
j ra
pop r8
pop r9
push sp
push ra
push r8
push r9
push r9
push r8
jal 1
pop r9
pop r8
move r10 r15
push r8
push r9
push r10
push r10
push 2
jal 9
pop r10
pop r9
pop r8
move r11 r15
move r15 r11
pop ra
pop sp
j ra

View File

@@ -0,0 +1,34 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 116
expression: output
---
## Unoptimized Output
j main
compare:
pop r8
pop r9
push sp
push ra
sgt r1 r9 r8
beqz r1 __internal_L2
move r10 1
__internal_L2:
__internal_L1:
pop ra
pop sp
j ra
## Optimized Output
j main
pop r8
pop r9
push sp
push ra
ble r9 r8 7
move r10 1
pop ra
pop sp
j ra

View File

@@ -0,0 +1,111 @@
---
source: libs/integration_tests/src/lib.rs
expression: output
---
## Unoptimized Output
j main
main:
s d2 Mode 1
s d2 On 0
move r8 0
move r9 0
__internal_L1:
yield
l r1 d0 Reagents
move r10 r1
sge r2 r10 100
sge r3 r9 2
or r4 r2 r3
beqz r4 __internal_L3
move r8 1
__internal_L3:
slt r5 r10 100
ls r15 d0 0 Occupied
seq r6 r15 0
and r7 r5 r6
add r1 r9 1
select r2 r7 r1 0
move r9 r2
l r3 d0 Rpm
slt r4 r3 1
and r5 r8 r4
beqz r5 __internal_L4
s d0 Open 1
seq r6 r10 0
ls r15 d0 0 Occupied
and r7 r6 r15
seq r1 r7 0
move r8 r1
__internal_L4:
seq r6 r8 0
s d0 On r6
ls r15 d1 0 Quantity
move r11 r15
l r7 d3 Pressure
sgt r1 r7 200
beqz r1 __internal_L5
j __internal_L1
__internal_L5:
sgt r2 r11 0
s d1 On r2
sgt r3 r11 0
s d1 Activate r3
l r4 d3 Pressure
sgt r5 r4 0
l r6 d1 Activate
or r7 r5 r6
s d2 On r7
l r1 d1 Activate
s db Setting r1
j __internal_L1
__internal_L2:
## Optimized Output
s d2 Mode 1
s d2 On 0
move r8 0
move r9 0
yield
l r10 d0 Reagents
sge r2 r10 100
sge r3 r9 2
or r4 r2 r3
beqz r4 11
move r8 1
slt r5 r10 100
ls r15 d0 0 Occupied
seq r6 r15 0
and r7 r5 r6
add r1 r9 1
select r2 r7 r1 0
move r9 r2
l r3 d0 Rpm
slt r4 r3 1
and r5 r8 r4
beqz r5 27
s d0 Open 1
seq r6 r10 0
ls r15 d0 0 Occupied
and r7 r6 r15
seq r8 r7 0
seq r6 r8 0
s d0 On r6
ls r15 d1 0 Quantity
move r11 r15
l r7 d3 Pressure
ble r7 200 34
j 4
sgt r2 r11 0
s d1 On r2
sgt r3 r11 0
s d1 Activate r3
l r4 d3 Pressure
sgt r5 r4 0
l r6 d1 Activate
or r7 r5 r6
s d2 On r7
l r1 d1 Activate
s db Setting r1
j 4

View File

@@ -0,0 +1,36 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 133
expression: output
---
## Unoptimized Output
j main
ternary:
pop r8
push sp
push ra
move r9 0
beqz r8 __internal_L3
move r9 10
j __internal_L2
__internal_L3:
move r9 20
__internal_L2:
move r15 r9
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
## Optimized Output
j main
pop r8
push sp
push ra
select r15 r8 10 20
pop ra
pop sp
j ra

View File

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

View File

@@ -0,0 +1,26 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 60
expression: output
---
## Unoptimized Output
j main
test:
push sp
push ra
move r8 10
__internal_L1:
pop ra
pop sp
j ra
## Optimized Output
j main
push sp
push ra
move r8 10
pop ra
pop sp
j ra

View File

@@ -0,0 +1,30 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 91
expression: output
---
## Unoptimized Output
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
## Optimized Output
j main
pop r8
push sp
push ra
add r15 r8 r8
pop ra
pop sp
j ra

View File

@@ -0,0 +1,93 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 206
expression: output
---
## Unoptimized Output
j main
getSomethingElse:
pop r8
push sp
push ra
add r1 r8 1
move r15 r1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
getSensorData:
push sp
push ra
l r1 d0 Vertical
push r1
l r2 d0 Horizontal
push r2
push 3
jal getSomethingElse
move r3 r15
push r3
sub r0 sp 5
get r0 db r0
move r15 r0
j __internal_L2
__internal_L2:
sub r0 sp 4
get ra db r0
j ra
main:
__internal_L3:
yield
jal getSensorData
pop r0
pop r9
pop r8
move sp r15
jal getSensorData
pop r0
pop r0
pop r9
move sp r15
s db Setting r9
j __internal_L3
__internal_L4:
## Optimized Output
j 23
pop r8
push sp
push ra
add r15 r8 1
pop ra
pop sp
j ra
push sp
push ra
l r1 d0 Vertical
push r1
l r2 d0 Horizontal
push r2
push 3
jal 1
move r3 r15
push r3
sub r0 sp 5
get r15 db r0
sub r0 sp 4
get ra db r0
j ra
yield
jal 8
pop r0
pop r9
pop r8
move sp r15
jal 8
pop r0
pop r0
pop r9
move sp r15
s db Setting r9
j 23

View File

@@ -0,0 +1,49 @@
device combustion = "d0";
device furnace = "d1";
device vent = "d2";
device gasSensor = "d3";
device self = "db";
const MAX_WAIT_ITER = 2;
const STACK_SIZE = 100;
vent.Mode = 1; // Vent inward into pipes
vent.On = false;
let ejecting = false;
let combustionWaitIter = 0;
loop {
yield();
let reagentCount = combustion.Reagents;
if (reagentCount >= STACK_SIZE || combustionWaitIter >= MAX_WAIT_ITER) {
ejecting = true;
}
combustionWaitIter = (reagentCount < STACK_SIZE && !ls(combustion, 0, "Occupied"))
? combustionWaitIter + 1
: 0;
if (ejecting && combustion.Rpm < 1) {
combustion.Open = true;
ejecting = !(reagentCount == 0 && ls(combustion, 0, "Occupied"));
}
combustion.On = !ejecting;
let furnaceAmt = ls(furnace, 0, "Quantity");
if (gasSensor.Pressure > 200) {
// safety: don't turn this on if we have gas still to process
// This should prevent pipes from blowing. This will NOT hault
// The in-progress burn job, but it'll prevent new jobs from
// blowing the walls or pipes.
continue;
}
furnace.On = furnaceAmt > 0;
furnace.Activate = furnaceAmt > 0;
vent.On = gasSensor.Pressure > 0 || furnace.Activate;
self.Setting = furnace.Activate;
}

View File

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

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

View File

@@ -0,0 +1,161 @@
use il::{Instruction, InstructionNode, Operand};
use rust_decimal::Decimal;
/// Pass: Algebraic Simplification
/// Simplifies mathematical identities like x+0, x*1, x*0, etc.
pub fn algebraic_simplification<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut output = Vec::with_capacity(input.len());
let mut changed = false;
for mut node in input {
let simplified = match &node.instruction {
// x + 0 = x
Instruction::Add(dst, a, Operand::Number(n)) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), a.clone()))
}
Instruction::Add(dst, Operand::Number(n), b) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), b.clone()))
}
// x - 0 = x
Instruction::Sub(dst, a, Operand::Number(n)) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), a.clone()))
}
// x * 1 = x
Instruction::Mul(dst, a, Operand::Number(n)) if *n == Decimal::from(1) => {
Some(Instruction::Move(dst.clone(), a.clone()))
}
Instruction::Mul(dst, Operand::Number(n), b) if *n == Decimal::from(1) => {
Some(Instruction::Move(dst.clone(), b.clone()))
}
// x * 0 = 0
Instruction::Mul(dst, _, Operand::Number(n)) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), Operand::Number(Decimal::ZERO)))
}
Instruction::Mul(dst, Operand::Number(n), _) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), Operand::Number(Decimal::ZERO)))
}
// x / 1 = x
Instruction::Div(dst, a, Operand::Number(n)) if *n == Decimal::from(1) => {
Some(Instruction::Move(dst.clone(), a.clone()))
}
// 0 / x = 0 (if x != 0, but we can't check at compile time for non-literals)
Instruction::Div(dst, Operand::Number(n), _) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), Operand::Number(Decimal::ZERO)))
}
// x % 1 = 0
Instruction::Mod(dst, _, Operand::Number(n)) if *n == Decimal::from(1) => {
Some(Instruction::Move(dst.clone(), Operand::Number(Decimal::ZERO)))
}
// 0 % x = 0
Instruction::Mod(dst, Operand::Number(n), _) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), Operand::Number(Decimal::ZERO)))
}
// x AND 0 = 0
Instruction::And(dst, _, Operand::Number(n)) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), Operand::Number(Decimal::ZERO)))
}
Instruction::And(dst, Operand::Number(n), _) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), Operand::Number(Decimal::ZERO)))
}
// x OR 0 = x
Instruction::Or(dst, a, Operand::Number(n)) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), a.clone()))
}
Instruction::Or(dst, Operand::Number(n), b) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), b.clone()))
}
// x XOR 0 = x
Instruction::Xor(dst, a, Operand::Number(n)) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), a.clone()))
}
Instruction::Xor(dst, Operand::Number(n), b) if n.is_zero() => {
Some(Instruction::Move(dst.clone(), b.clone()))
}
_ => None,
};
if let Some(new) = simplified {
node.instruction = new;
changed = true;
}
output.push(node);
}
(output, changed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_zero() {
let input = vec![InstructionNode::new(
Instruction::Add(
Operand::Register(1),
Operand::Register(2),
Operand::Number(Decimal::ZERO),
),
None,
)];
let (output, changed) = algebraic_simplification(input);
assert!(changed);
assert!(matches!(
output[0].instruction,
Instruction::Move(Operand::Register(1), Operand::Register(2))
));
}
#[test]
fn test_mul_one() {
let input = vec![InstructionNode::new(
Instruction::Mul(
Operand::Register(3),
Operand::Register(4),
Operand::Number(Decimal::ONE),
),
None,
)];
let (output, changed) = algebraic_simplification(input);
assert!(changed);
assert!(matches!(
output[0].instruction,
Instruction::Move(Operand::Register(3), Operand::Register(4))
));
}
#[test]
fn test_mul_zero() {
let input = vec![InstructionNode::new(
Instruction::Mul(
Operand::Register(5),
Operand::Register(6),
Operand::Number(Decimal::ZERO),
),
None,
)];
let (output, changed) = algebraic_simplification(input);
assert!(changed);
assert!(matches!(
output[0].instruction,
Instruction::Move(Operand::Register(5), Operand::Number(_))
));
}
}

View File

@@ -0,0 +1,156 @@
use crate::helpers::get_destination_reg;
use il::{Instruction, InstructionNode, Operand};
use rust_decimal::Decimal;
/// Pass: Constant Propagation
/// Folds arithmetic operations when both operands are constant.
/// Also tracks register values and propagates them forward.
pub 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 {
// Invalidate register tracking on label/call boundaries
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() { Decimal::ZERO } else { x / y }
}),
Instruction::Mod(dst, a, b) => try_fold_math(dst, a, b, &registers, |x, y| {
if y.is_zero() { Decimal::ZERO } 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 register tracking
match &node.instruction {
Instruction::Move(Operand::Register(r), src) => {
registers[*r as usize] = resolve_value(src, &registers)
}
_ => {
if let Some(r) = get_destination_reg(&node.instruction) {
registers[r as usize] = None;
}
}
}
// Filter out NOPs (empty labels 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
}
}
#[cfg(test)]
mod tests {
use super::*;
use il::InstructionNode;
#[test]
fn test_fold_add() {
let input = vec![InstructionNode::new(
Instruction::Add(
Operand::Register(1),
Operand::Number(5.into()),
Operand::Number(3.into()),
),
None,
)];
let (output, changed) = constant_propagation(input);
assert!(changed);
assert_eq!(output.len(), 1);
assert!(matches!(
output[0].instruction,
Instruction::Move(Operand::Register(1), Operand::Number(_))
));
}
}

View File

@@ -0,0 +1,148 @@
use il::{Instruction, InstructionNode, Operand};
/// Pass: Redundant Move Elimination
/// Removes moves where source and destination are the same: `move rx rx`
pub 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)
}
/// Pass: Dead Code Elimination
/// Removes unreachable code after unconditional jumps.
pub 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)
}
/// Pass: Remove Redundant Jumps
/// Removes jumps to the next instruction (after label resolution).
/// Must run AFTER label resolution since it needs line numbers.
/// This pass also adjusts all jump targets to account for removed instructions.
pub fn remove_redundant_jumps<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut output = Vec::with_capacity(input.len());
let mut changed = false;
let mut removed_lines = Vec::new();
// First pass: identify redundant jumps
for (i, node) in input.iter().enumerate() {
// Check if this is a jump to the next line number
if let Instruction::Jump(Operand::Number(target)) = &node.instruction {
// Current line number is i, next line number is i+1
// If jump target equals the next line, it's redundant
if target.to_string().parse::<usize>().ok() == Some(i + 1) {
changed = true;
removed_lines.push(i);
continue; // Skip this redundant jump
}
}
output.push(node.clone());
}
// Second pass: adjust all jump/branch targets to account for removed lines
if changed {
for node in &mut output {
// Helper to adjust a target line number
let adjust_target = |target_line: usize| -> usize {
// Count how many removed lines are before the target
let offset = removed_lines
.iter()
.filter(|&&removed| removed < target_line)
.count();
target_line.saturating_sub(offset)
};
match &mut node.instruction {
Instruction::Jump(Operand::Number(target))
| Instruction::JumpAndLink(Operand::Number(target)) => {
if let Ok(target_line) = target.to_string().parse::<usize>() {
*target = rust_decimal::Decimal::from(adjust_target(target_line));
}
}
Instruction::BranchEq(_, _, Operand::Number(target))
| Instruction::BranchNe(_, _, Operand::Number(target))
| Instruction::BranchGt(_, _, Operand::Number(target))
| Instruction::BranchLt(_, _, Operand::Number(target))
| Instruction::BranchGe(_, _, Operand::Number(target))
| Instruction::BranchLe(_, _, Operand::Number(target))
| Instruction::BranchEqZero(_, Operand::Number(target))
| Instruction::BranchNeZero(_, Operand::Number(target)) => {
if let Ok(target_line) = target.to_string().parse::<usize>() {
*target = rust_decimal::Decimal::from(adjust_target(target_line));
}
}
_ => {}
}
}
}
(output, changed)
}
#[cfg(test)]
mod tests {
use super::*;
use il::{Instruction, InstructionNode, Operand};
#[test]
fn test_remove_redundant_move() {
let input = vec![InstructionNode::new(
Instruction::Move(Operand::Register(1), Operand::Register(1)),
None,
)];
let (output, changed) = remove_redundant_moves(input);
assert!(changed);
assert_eq!(output.len(), 0);
}
#[test]
fn test_remove_unreachable() {
let input = vec![
InstructionNode::new(Instruction::Jump(Operand::Label("main".into())), None),
InstructionNode::new(
Instruction::Add(
Operand::Register(1),
Operand::Number(1.into()),
Operand::Number(2.into()),
),
None,
),
InstructionNode::new(Instruction::LabelDef("main".into()), None),
];
let (output, changed) = remove_unreachable_code(input);
assert!(changed);
assert_eq!(output.len(), 2);
}
}

View File

@@ -0,0 +1,126 @@
use crate::helpers::get_destination_reg;
use il::{Instruction, InstructionNode};
use std::collections::HashMap;
/// Pass: Dead Store Elimination
/// Removes writes to registers that are never read before being overwritten.
pub fn dead_store_elimination<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
// Forward pass: Remove writes that are immediately overwritten
let (input, forward_changed) = eliminate_overwritten_stores(input);
// Note: Backward pass disabled for now - it needs more work to handle all cases correctly
// The forward pass is sufficient for most common patterns
// (e.g., move r6 r15 immediately followed by move r6 r15 again)
(input, forward_changed)
}
/// Forward pass: Remove stores that are overwritten before being read
fn eliminate_overwritten_stores<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut last_write: HashMap<u8, usize> = HashMap::new();
let mut to_remove = Vec::new();
// Scan for dead writes
for (i, node) in input.iter().enumerate() {
// Never remove Pop instructions - they have critical side effects on the stack pointer
if matches!(node.instruction, Instruction::Pop(_)) {
continue;
}
if let Some(dest_reg) = get_destination_reg(&node.instruction) {
// If this register was written before and hasn't been read, previous write is dead
if let Some(&prev_idx) = last_write.get(&dest_reg) {
// Check if the value was ever used between prev_idx and current
let was_used = input[prev_idx + 1..i]
.iter()
.any(|n| reg_is_read_or_affects_control(&n.instruction, dest_reg))
// Also check if current instruction reads the register before overwriting it
|| reg_is_read_or_affects_control(&node.instruction, dest_reg);
if !was_used {
// Previous write was dead
to_remove.push(prev_idx);
}
}
// Update last write location
last_write.insert(dest_reg, i);
}
// Handle control flow instructions
match &node.instruction {
// JumpAndLink (function calls) only clobbers the return register (r15)
// We can continue tracking other registers across function calls
Instruction::JumpAndLink(_) => {
last_write.remove(&15);
}
// Other control flow instructions create complexity - clear all tracking
Instruction::Jump(_)
| Instruction::LabelDef(_)
| Instruction::BranchEq(_, _, _)
| Instruction::BranchNe(_, _, _)
| Instruction::BranchGt(_, _, _)
| Instruction::BranchLt(_, _, _)
| Instruction::BranchGe(_, _, _)
| Instruction::BranchLe(_, _, _)
| Instruction::BranchEqZero(_, _)
| Instruction::BranchNeZero(_, _) => {
last_write.clear();
}
_ => {}
}
}
if !to_remove.is_empty() {
let output = input
.into_iter()
.enumerate()
.filter_map(|(i, node)| {
if to_remove.contains(&i) {
None
} else {
Some(node)
}
})
.collect();
(output, true)
} else {
(input, false)
}
}
/// Simplified check: Does this instruction read the register?
fn reg_is_read_or_affects_control(instr: &Instruction, reg: u8) -> bool {
use crate::helpers::reg_is_read;
// If it reads the register, it's used
reg_is_read(instr, reg)
}
#[cfg(test)]
mod tests {
use super::*;
use il::Operand;
#[test]
fn test_dead_store() {
let input = vec![
InstructionNode::new(
Instruction::Move(Operand::Register(1), Operand::Number(5.into())),
None,
),
InstructionNode::new(
Instruction::Move(Operand::Register(1), Operand::Number(10.into())),
None,
),
];
let (output, changed) = dead_store_elimination(input);
assert!(changed);
assert_eq!(output.len(), 1);
}
}

View File

@@ -0,0 +1,160 @@
use crate::helpers::get_destination_reg;
use il::{Instruction, InstructionNode, Operand};
use rust_decimal::Decimal;
use std::collections::{HashMap, HashSet};
/// 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.
pub 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!
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);
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 {
// Remove all pushes/restores
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;
}
node.instruction = Instruction::Sub(dst.clone(), a.clone(), Operand::Number(new_n));
}
clean.push(node);
}
return (clean, changed);
}
(input, false)
}

View File

@@ -0,0 +1,177 @@
use il::{Instruction, Operand};
/// Returns the register number written to by an instruction, if any.
pub 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,
}
}
/// Creates a new instruction with the destination register changed.
pub fn set_destination_reg<'a>(instr: &Instruction<'a>, new_reg: u8) -> Option<Instruction<'a>> {
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)),
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,
}
}
/// Checks if a register is read by an instruction.
pub 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),
Instruction::Store(a, _, b) => check(a) || check(b),
Instruction::StoreBatch(a, _, b) => check(a) || check(b),
Instruction::StoreBatchNamed(a, b, _, c) => check(a) || check(b) || check(c),
Instruction::StoreSlot(a, b, _, c) => check(a) || check(b) || check(c),
Instruction::BranchEq(a, b, _)
| Instruction::BranchNe(a, b, _)
| Instruction::BranchGt(a, b, _)
| 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),
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),
Instruction::Atan2(_, a, b) | Instruction::Max(_, a, b) | Instruction::Min(_, a, b) => {
check(a) || check(b)
}
Instruction::JumpRelative(a) => check(a),
Instruction::Alias(_, a) => check(a),
_ => false,
}
}

View File

@@ -0,0 +1,70 @@
use il::{Instruction, InstructionNode, Operand};
use rust_decimal::Decimal;
use std::collections::HashMap;
/// Pass: Resolve Labels
/// Converts all Jump/Branch labels to absolute line numbers and removes LabelDefs.
pub 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;
// 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());
// 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
}

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,41 @@
use crate::leaf_function::find_leaf_functions;
use il::InstructionNode;
/// Pass: Leaf Function Optimization
/// If a function makes no calls (is a leaf), it doesn't need to save/restore `ra`.
///
/// NOTE: This optimization is DISABLED due to correctness issues.
/// The optimization was designed for a specific calling convention (GET/PUT for RA)
/// but the compiler generates POP ra for return address restoration. Without proper
/// tracking of both conventions and validation of balanced push/pop pairs, this
/// optimization corrupts the stack frame by:
///
/// 1. Removing `push ra` but not `pop ra`, leaving unbalanced push/pop pairs
/// 2. Not accounting for parameter pops that occur before `push sp`
/// 3. Assuming all RA restoration uses GET instruction, but code uses POP
///
/// Example of broken optimization:
/// ```
/// Unoptimized: Optimized (BROKEN):
/// compare: pop r8
/// pop r8 pop r9
/// pop r9 ble r9 r8 5
/// push sp move r10 1
/// push ra j ra
/// sgt r1 r9 r8 ^ Missing stack frame!
/// ...
/// pop ra
/// pop sp
/// j ra
/// ```
///
/// Future work: Fix by handling both POP and GET calling conventions, validating
/// balanced push/pop pairs, and accounting for parameter pops.
pub fn optimize_leaf_functions<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
// Optimization disabled - returns input unchanged
#[allow(unused)]
let _leaves = find_leaf_functions(&input);
(input, false)
}

View File

@@ -0,0 +1,100 @@
use il::Instructions;
// Optimization pass modules
mod helpers;
mod leaf_function;
mod algebraic_simplification;
mod constant_propagation;
mod dead_code;
mod dead_store_elimination;
mod function_call_optimization;
mod label_resolution;
mod leaf_function_optimization;
mod peephole_optimization;
mod register_forwarding;
mod strength_reduction;
use algebraic_simplification::algebraic_simplification;
use constant_propagation::constant_propagation;
use dead_code::{remove_redundant_jumps, remove_redundant_moves, remove_unreachable_code};
use dead_store_elimination::dead_store_elimination;
use function_call_optimization::optimize_function_calls;
use label_resolution::resolve_labels;
use leaf_function_optimization::optimize_leaf_functions;
use peephole_optimization::peephole_optimization;
use register_forwarding::register_forwarding;
use strength_reduction::strength_reduction;
/// 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: Algebraic Simplification (Identity operations)
let (new_inst, c5) = algebraic_simplification(instructions);
instructions = new_inst;
changed |= c5;
// Pass 6: Strength Reduction (Replace expensive ops with cheaper ones)
let (new_inst, c6) = strength_reduction(instructions);
instructions = new_inst;
changed |= c6;
// Pass 7: Peephole Optimizations (Common patterns)
let (new_inst, c7) = peephole_optimization(instructions);
instructions = new_inst;
changed |= c7;
// Pass 8: Dead Store Elimination
let (new_inst, c8) = dead_store_elimination(instructions);
instructions = new_inst;
changed |= c8;
// Pass 9: Redundant Move Elimination
let (new_inst, c9) = remove_redundant_moves(instructions);
instructions = new_inst;
changed |= c9;
// Pass 10: Dead Code Elimination
let (new_inst, c10) = remove_unreachable_code(instructions);
instructions = new_inst;
changed |= c10;
}
// Final Pass: Resolve Labels to Line Numbers
let instructions = resolve_labels(instructions);
// Post-resolution Pass: Remove redundant jumps (must run after label resolution)
let (instructions, _) = remove_redundant_jumps(instructions);
Instructions::new(instructions)
}

View File

@@ -0,0 +1,761 @@
use il::{Instruction, InstructionNode, Operand};
/// Pass: Peephole Optimization
/// Recognizes and optimizes common instruction patterns.
pub fn peephole_optimization<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut output = Vec::with_capacity(input.len());
let mut changed = false;
let mut i = 0;
while i < input.len() {
// Pattern: push sp; push ra ... pop ra; pop sp (with no jal in between)
// If we push sp and ra and later pop them, but never call a function in between, remove all four
// and adjust any stack pointer offsets in between by -2
if i + 1 < input.len() {
if let (
Instruction::Push(Operand::StackPointer),
Instruction::Push(Operand::ReturnAddress),
) = (&input[i].instruction, &input[i + 1].instruction)
{
// Look for matching pop ra; pop sp pattern
if let Some((ra_pop_idx, instructions_between)) =
find_matching_ra_pop(&input[i + 1..])
{
let absolute_ra_pop = i + 1 + ra_pop_idx;
// Check if the next instruction is pop sp
if absolute_ra_pop + 1 < input.len() {
if let Instruction::Pop(Operand::StackPointer) =
&input[absolute_ra_pop + 1].instruction
{
// Check if there's any jal between push and pop
let has_call = instructions_between.iter().any(|node| {
matches!(node.instruction, Instruction::JumpAndLink(_))
});
if !has_call {
// Safe to remove all four: push sp, push ra, pop ra, pop sp
// Also need to adjust stack pointer offsets in between by -2
let absolute_sp_pop = absolute_ra_pop + 1;
// Clear output since we're going to reprocess the entire input
output.clear();
for (idx, node) in input.iter().enumerate() {
if idx == i
|| idx == i + 1
|| idx == absolute_ra_pop
|| idx == absolute_sp_pop
{
// Skip all four push/pop instructions
continue;
}
// If this instruction is between the pushes and pops, adjust its stack offsets
if idx > i + 1 && idx < absolute_ra_pop {
let adjusted_instruction =
adjust_stack_offset(node.instruction.clone(), 2);
output.push(InstructionNode::new(
adjusted_instruction,
node.span,
));
} else {
output.push(node.clone());
}
}
changed = true;
// We've processed the entire input, so break
break;
}
}
}
}
}
}
// Pattern: push ra ... pop ra (with no jal in between)
// Fallback for when there's only ra push/pop without sp
if let Instruction::Push(Operand::ReturnAddress) = &input[i].instruction {
if let Some((pop_idx, instructions_between)) = find_matching_ra_pop(&input[i..]) {
// Check if there's any jal between push and pop
let has_call = instructions_between
.iter()
.any(|node| matches!(node.instruction, Instruction::JumpAndLink(_)));
if !has_call {
// Safe to remove both push and pop
// Also need to adjust stack pointer offsets in between
let absolute_pop_idx = i + pop_idx;
// Clear output since we're going to reprocess the entire input
output.clear();
for (idx, node) in input.iter().enumerate() {
if idx == i || idx == absolute_pop_idx {
// Skip the push and pop
continue;
}
// If this instruction is between push and pop, adjust its stack offsets
if idx > i && idx < absolute_pop_idx {
let adjusted_instruction =
adjust_stack_offset(node.instruction.clone(), 1);
output.push(InstructionNode::new(adjusted_instruction, node.span));
} else {
output.push(node.clone());
}
}
changed = true;
// We've processed the entire input, so break
break;
}
}
}
// Pattern: Branch-Move-Jump-Label-Move-Label -> Select
// beqz r1 else_label
// move r2 val1
// j end_label
// else_label:
// move r2 val2
// end_label:
// Converts to: select r2 r1 val1 val2
if i + 5 < input.len() {
let select_pattern = try_match_select_pattern(&input[i..i + 6]);
if let Some((dst, cond, true_val, false_val, skip_count)) = select_pattern {
output.push(InstructionNode::new(
Instruction::Select(dst, cond, true_val, false_val),
input[i].span,
));
changed = true;
i += skip_count;
continue;
}
}
// Pattern: seq + beqz -> beq
if i + 1 < input.len() {
let pattern = match (&input[i].instruction, &input[i + 1].instruction) {
(
Instruction::SetEq(Operand::Register(temp), a, b),
Instruction::BranchEqZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Eq, true)), // invert: beqz means "if NOT equal"
(
Instruction::SetNe(Operand::Register(temp), a, b),
Instruction::BranchEqZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Ne, true)),
(
Instruction::SetGt(Operand::Register(temp), a, b),
Instruction::BranchEqZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Gt, true)),
(
Instruction::SetLt(Operand::Register(temp), a, b),
Instruction::BranchEqZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Lt, true)),
(
Instruction::SetGe(Operand::Register(temp), a, b),
Instruction::BranchEqZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Ge, true)),
(
Instruction::SetLe(Operand::Register(temp), a, b),
Instruction::BranchEqZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Le, true)),
// Pattern: seq + bnez -> bne
(
Instruction::SetEq(Operand::Register(temp), a, b),
Instruction::BranchNeZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Eq, false)),
(
Instruction::SetNe(Operand::Register(temp), a, b),
Instruction::BranchNeZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Ne, false)),
(
Instruction::SetGt(Operand::Register(temp), a, b),
Instruction::BranchNeZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Gt, false)),
(
Instruction::SetLt(Operand::Register(temp), a, b),
Instruction::BranchNeZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Lt, false)),
(
Instruction::SetGe(Operand::Register(temp), a, b),
Instruction::BranchNeZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Ge, false)),
(
Instruction::SetLe(Operand::Register(temp), a, b),
Instruction::BranchNeZero(Operand::Register(cond), label),
) if temp == cond => Some((a, b, label, BranchType::Le, false)),
_ => None,
};
if let Some((a, b, label, branch_type, invert)) = pattern {
// Create optimized branch instruction
let new_instr = if invert {
// beqz after seq means "branch if NOT equal" -> bne
match branch_type {
BranchType::Eq => {
Instruction::BranchNe(a.clone(), b.clone(), label.clone())
}
BranchType::Ne => {
Instruction::BranchEq(a.clone(), b.clone(), label.clone())
}
BranchType::Gt => {
Instruction::BranchLe(a.clone(), b.clone(), label.clone())
}
BranchType::Lt => {
Instruction::BranchGe(a.clone(), b.clone(), label.clone())
}
BranchType::Ge => {
Instruction::BranchLt(a.clone(), b.clone(), label.clone())
}
BranchType::Le => {
Instruction::BranchGt(a.clone(), b.clone(), label.clone())
}
}
} else {
// bnez after seq means "branch if equal" -> beq
match branch_type {
BranchType::Eq => {
Instruction::BranchEq(a.clone(), b.clone(), label.clone())
}
BranchType::Ne => {
Instruction::BranchNe(a.clone(), b.clone(), label.clone())
}
BranchType::Gt => {
Instruction::BranchGt(a.clone(), b.clone(), label.clone())
}
BranchType::Lt => {
Instruction::BranchLt(a.clone(), b.clone(), label.clone())
}
BranchType::Ge => {
Instruction::BranchGe(a.clone(), b.clone(), label.clone())
}
BranchType::Le => {
Instruction::BranchLe(a.clone(), b.clone(), label.clone())
}
}
};
output.push(InstructionNode::new(new_instr, input[i].span));
changed = true;
i += 2; // Skip both instructions
continue;
}
}
output.push(input[i].clone());
i += 1;
}
(output, changed)
}
/// Tries to match a select pattern in the instruction sequence.
/// Pattern (6 instructions):
/// beqz/bnez cond else_label (i+0)
/// move dst val1 (i+1)
/// j end_label (i+2)
/// else_label: (i+3)
/// move dst val2 (i+4)
/// end_label: (i+5)
/// Returns: (dst, cond, true_val, false_val, instruction_count)
fn try_match_select_pattern<'a>(
instructions: &[InstructionNode<'a>],
) -> Option<(Operand<'a>, Operand<'a>, Operand<'a>, Operand<'a>, usize)> {
if instructions.len() < 6 {
return None;
}
// Check for beqz pattern
if let Instruction::BranchEqZero(cond, Operand::Label(else_label)) =
&instructions[0].instruction
{
if let Instruction::Move(dst1, val1) = &instructions[1].instruction {
if let Instruction::Jump(Operand::Label(end_label)) = &instructions[2].instruction {
if let Instruction::LabelDef(label3) = &instructions[3].instruction {
if label3 == else_label {
if let Instruction::Move(dst2, val2) = &instructions[4].instruction {
if dst1 == dst2 {
if let Instruction::LabelDef(label5) = &instructions[5].instruction
{
if label5 == end_label {
// beqz means: if cond==0, goto else, so val1 is for true, val2 for false
// select dst cond true_val false_val
// When cond is non-zero (true), use val1, otherwise val2
return Some((
dst1.clone(),
cond.clone(),
val1.clone(),
val2.clone(),
6,
));
}
}
}
}
}
}
}
}
}
// Check for bnez pattern
if let Instruction::BranchNeZero(cond, Operand::Label(then_label)) =
&instructions[0].instruction
{
if let Instruction::Move(dst1, val_false) = &instructions[1].instruction {
if let Instruction::Jump(Operand::Label(end_label)) = &instructions[2].instruction {
if let Instruction::LabelDef(label3) = &instructions[3].instruction {
if label3 == then_label {
if let Instruction::Move(dst2, val_true) = &instructions[4].instruction {
if dst1 == dst2 {
if let Instruction::LabelDef(label5) = &instructions[5].instruction
{
if label5 == end_label {
// bnez means: if cond!=0, goto then, so val_true for true, val_false for false
return Some((
dst1.clone(),
cond.clone(),
val_true.clone(),
val_false.clone(),
6,
));
}
}
}
}
}
}
}
}
}
None
}
/// Finds a matching `pop ra` for a `push ra` at the start of the slice.
/// Returns the index of the pop and the instructions in between.
fn find_matching_ra_pop<'a>(
instructions: &'a [InstructionNode<'a>],
) -> Option<(usize, &'a [InstructionNode<'a>])> {
if instructions.is_empty() {
return None;
}
// Skip the push itself
for (idx, node) in instructions.iter().enumerate().skip(1) {
if let Instruction::Pop(Operand::ReturnAddress) = &node.instruction {
// Found matching pop
return Some((idx, &instructions[1..idx]));
}
// Stop searching if we hit a jump (different control flow) or a function label
// Labels are OK - they're just markers EXCEPT for user-defined function labels
// which indicate a function boundary
if matches!(
node.instruction,
Instruction::Jump(_) | Instruction::JumpRelative(_) | Instruction::LabelDef(_)
) {
return None;
}
}
None
}
/// Checks if an instruction uses or modifies the stack pointer.
#[allow(dead_code)]
fn uses_stack_pointer(instruction: &Instruction) -> bool {
match instruction {
Instruction::Push(_) | Instruction::Pop(_) | Instruction::Peek(_) => true,
Instruction::Add(Operand::StackPointer, _, _)
| Instruction::Sub(Operand::StackPointer, _, _)
| Instruction::Mul(Operand::StackPointer, _, _)
| Instruction::Div(Operand::StackPointer, _, _)
| Instruction::Mod(Operand::StackPointer, _, _) => true,
Instruction::Add(_, Operand::StackPointer, _)
| Instruction::Sub(_, Operand::StackPointer, _)
| Instruction::Mul(_, Operand::StackPointer, _)
| Instruction::Div(_, Operand::StackPointer, _)
| Instruction::Mod(_, Operand::StackPointer, _) => true,
Instruction::Add(_, _, Operand::StackPointer)
| Instruction::Sub(_, _, Operand::StackPointer)
| Instruction::Mul(_, _, Operand::StackPointer)
| Instruction::Div(_, _, Operand::StackPointer)
| Instruction::Mod(_, _, Operand::StackPointer) => true,
Instruction::Move(Operand::StackPointer, _)
| Instruction::Move(_, Operand::StackPointer) => true,
_ => false,
}
}
/// Adjusts stack pointer offsets in an instruction by decrementing them by a given amount.
/// This is necessary when removing push operations that would have increased the stack size.
fn adjust_stack_offset<'a>(instruction: Instruction<'a>, decrement: i64) -> Instruction<'a> {
use rust_decimal::prelude::*;
match instruction {
// Adjust arithmetic operations on sp that use literal offsets
Instruction::Sub(dst, Operand::StackPointer, Operand::Number(n)) => {
let new_n = n - Decimal::from(decrement);
// If the result is 0 or negative, we may want to skip this entirely
// but for now, just adjust the value
Instruction::Sub(dst, Operand::StackPointer, Operand::Number(new_n))
}
Instruction::Add(dst, Operand::StackPointer, Operand::Number(n)) => {
let new_n = n - Decimal::from(decrement);
Instruction::Add(dst, Operand::StackPointer, Operand::Number(new_n))
}
// Return the instruction unchanged if it doesn't need adjustment
other => other,
}
}
#[derive(Debug, Clone, Copy)]
enum BranchType {
Eq,
Ne,
Gt,
Lt,
Ge,
Le,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_seq_beqz_to_bne() {
let input = vec![
InstructionNode::new(
Instruction::SetEq(
Operand::Register(1),
Operand::Register(2),
Operand::Register(3),
),
None,
),
InstructionNode::new(
Instruction::BranchEqZero(Operand::Register(1), Operand::Label("target".into())),
None,
),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
assert!(matches!(
output[0].instruction,
Instruction::BranchNe(_, _, _)
));
}
#[test]
fn test_sne_beqz_to_beq() {
let input = vec![
InstructionNode::new(
Instruction::SetNe(
Operand::Register(1),
Operand::Register(2),
Operand::Register(3),
),
None,
),
InstructionNode::new(
Instruction::BranchEqZero(Operand::Register(1), Operand::Label("target".into())),
None,
),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
assert!(matches!(
output[0].instruction,
Instruction::BranchEq(_, _, _)
));
}
#[test]
fn test_seq_bnez_to_beq() {
let input = vec![
InstructionNode::new(
Instruction::SetEq(
Operand::Register(1),
Operand::Register(2),
Operand::Register(3),
),
None,
),
InstructionNode::new(
Instruction::BranchNeZero(Operand::Register(1), Operand::Label("target".into())),
None,
),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
assert!(matches!(
output[0].instruction,
Instruction::BranchEq(_, _, _)
));
}
#[test]
fn test_sgt_beqz_to_ble() {
let input = vec![
InstructionNode::new(
Instruction::SetGt(
Operand::Register(1),
Operand::Register(2),
Operand::Register(3),
),
None,
),
InstructionNode::new(
Instruction::BranchEqZero(Operand::Register(1), Operand::Label("target".into())),
None,
),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
assert!(matches!(
output[0].instruction,
Instruction::BranchLe(_, _, _)
));
}
#[test]
fn test_branch_move_jump_to_select_beqz() {
// Pattern: beqz r1 else / move r2 10 / j end / else: / move r2 20 / end:
// Should convert to: select r2 r1 10 20
let input = vec![
InstructionNode::new(
Instruction::BranchEqZero(Operand::Register(1), Operand::Label("else".into())),
None,
),
InstructionNode::new(
Instruction::Move(Operand::Register(2), Operand::Number(10.into())),
None,
),
InstructionNode::new(Instruction::Jump(Operand::Label("end".into())), None),
InstructionNode::new(Instruction::LabelDef("else".into()), None),
InstructionNode::new(
Instruction::Move(Operand::Register(2), Operand::Number(20.into())),
None,
),
InstructionNode::new(Instruction::LabelDef("end".into()), None),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
if let Instruction::Select(dst, cond, true_val, false_val) = &output[0].instruction {
assert!(matches!(dst, Operand::Register(2)));
assert!(matches!(cond, Operand::Register(1)));
assert!(matches!(true_val, Operand::Number(_)));
assert!(matches!(false_val, Operand::Number(_)));
} else {
panic!("Expected Select instruction");
}
}
#[test]
fn test_branch_move_jump_to_select_bnez() {
// Pattern: bnez r1 then / move r2 20 / j end / then: / move r2 10 / end:
// Should convert to: select r2 r1 10 20
let input = vec![
InstructionNode::new(
Instruction::BranchNeZero(Operand::Register(1), Operand::Label("then".into())),
None,
),
InstructionNode::new(
Instruction::Move(Operand::Register(2), Operand::Number(20.into())),
None,
),
InstructionNode::new(Instruction::Jump(Operand::Label("end".into())), None),
InstructionNode::new(Instruction::LabelDef("then".into()), None),
InstructionNode::new(
Instruction::Move(Operand::Register(2), Operand::Number(10.into())),
None,
),
InstructionNode::new(Instruction::LabelDef("end".into()), None),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
if let Instruction::Select(dst, cond, true_val, false_val) = &output[0].instruction {
assert!(matches!(dst, Operand::Register(2)));
assert!(matches!(cond, Operand::Register(1)));
assert!(matches!(true_val, Operand::Number(_)));
assert!(matches!(false_val, Operand::Number(_)));
} else {
panic!("Expected Select instruction");
}
}
#[test]
fn test_remove_useless_ra_push_pop() {
// Pattern: push ra / add r1 r2 r3 / pop ra
// Should remove both push and pop since no jal in between
let input = vec![
InstructionNode::new(Instruction::Push(Operand::ReturnAddress), None),
InstructionNode::new(
Instruction::Add(
Operand::Register(1),
Operand::Register(2),
Operand::Register(3),
),
None,
),
InstructionNode::new(Instruction::Pop(Operand::ReturnAddress), None),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
assert!(matches!(output[0].instruction, Instruction::Add(_, _, _)));
}
#[test]
fn test_keep_ra_push_pop_with_jal() {
// Pattern: push ra / jal func / pop ra
// Should keep both since there's a jal in between
let input = vec![
InstructionNode::new(Instruction::Push(Operand::ReturnAddress), None),
InstructionNode::new(
Instruction::JumpAndLink(Operand::Label("func".into())),
None,
),
InstructionNode::new(Instruction::Pop(Operand::ReturnAddress), None),
];
let (output, changed) = peephole_optimization(input);
assert!(!changed);
assert_eq!(output.len(), 3);
}
#[test]
fn test_ra_push_pop_with_stack_offset_adjustment() {
// Pattern: push ra / sub r1 sp 2 / pop ra
// Should remove push/pop AND adjust the stack offset from 2 to 1
use rust_decimal::prelude::*;
let input = vec![
InstructionNode::new(Instruction::Push(Operand::ReturnAddress), None),
InstructionNode::new(
Instruction::Sub(
Operand::Register(1),
Operand::StackPointer,
Operand::Number(Decimal::from(2)),
),
None,
),
InstructionNode::new(Instruction::Pop(Operand::ReturnAddress), None),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
if let Instruction::Sub(dst, src, Operand::Number(offset)) = &output[0].instruction {
assert!(matches!(dst, Operand::Register(1)));
assert!(matches!(src, Operand::StackPointer));
assert_eq!(*offset, Decimal::from(1)); // Should be decremented from 2 to 1
} else {
panic!("Expected Sub instruction with adjusted offset");
}
}
#[test]
fn test_remove_sp_and_ra_push_pop() {
// Pattern: push sp / push ra / move r8 10 / pop ra / pop sp
// Should remove all four push/pop instructions since no jal in between
let input = vec![
InstructionNode::new(Instruction::Push(Operand::StackPointer), None),
InstructionNode::new(Instruction::Push(Operand::ReturnAddress), None),
InstructionNode::new(
Instruction::Move(Operand::Register(8), Operand::Number(10.into())),
None,
),
InstructionNode::new(Instruction::Pop(Operand::ReturnAddress), None),
InstructionNode::new(Instruction::Pop(Operand::StackPointer), None),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
assert!(matches!(
output[0].instruction,
Instruction::Move(Operand::Register(8), _)
));
}
#[test]
fn test_keep_sp_and_ra_push_pop_with_jal() {
// Pattern: push sp / push ra / jal func / pop ra / pop sp
// Should keep all since there's a jal in between
let input = vec![
InstructionNode::new(Instruction::Push(Operand::StackPointer), None),
InstructionNode::new(Instruction::Push(Operand::ReturnAddress), None),
InstructionNode::new(
Instruction::JumpAndLink(Operand::Label("func".into())),
None,
),
InstructionNode::new(Instruction::Pop(Operand::ReturnAddress), None),
InstructionNode::new(Instruction::Pop(Operand::StackPointer), None),
];
let (output, changed) = peephole_optimization(input);
assert!(!changed);
assert_eq!(output.len(), 5);
}
#[test]
fn test_sp_and_ra_with_stack_offset_adjustment() {
// Pattern: push sp / push ra / sub r1 sp 3 / pop ra / pop sp
// Should remove all push/pop AND adjust the stack offset from 3 to 1 (decrement by 2)
use rust_decimal::prelude::*;
let input = vec![
InstructionNode::new(Instruction::Push(Operand::StackPointer), None),
InstructionNode::new(Instruction::Push(Operand::ReturnAddress), None),
InstructionNode::new(
Instruction::Sub(
Operand::Register(1),
Operand::StackPointer,
Operand::Number(Decimal::from(3)),
),
None,
),
InstructionNode::new(Instruction::Pop(Operand::ReturnAddress), None),
InstructionNode::new(Instruction::Pop(Operand::StackPointer), None),
];
let (output, changed) = peephole_optimization(input);
assert!(changed);
assert_eq!(output.len(), 1);
if let Instruction::Sub(dst, src, Operand::Number(offset)) = &output[0].instruction {
assert!(matches!(dst, Operand::Register(1)));
assert!(matches!(src, Operand::StackPointer));
assert_eq!(*offset, Decimal::from(1)); // Should be decremented from 3 to 1
} else {
panic!("Expected Sub instruction with adjusted offset");
}
}
}

View File

@@ -0,0 +1,151 @@
use crate::helpers::{get_destination_reg, reg_is_read, set_destination_reg};
use il::{Instruction, InstructionNode, Operand};
use std::collections::HashMap;
/// Pass: Register Forwarding
/// Eliminates intermediate moves by writing directly to the final destination.
/// Example: `l r1 d0 Temperature` + `move r9 r1` -> `l r9 d0 Temperature`
pub fn register_forwarding<'a>(
mut input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut changed = false;
let mut i = 0;
// Build a map of label positions to detect backward jumps
// Use String keys to avoid lifetime issues with references into input
let label_positions: HashMap<String, usize> = input
.iter()
.enumerate()
.filter_map(|(idx, node)| {
if let Instruction::LabelDef(label) = &node.instruction {
Some((label.to_string(), idx))
} else {
None
}
})
.collect();
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(
il::Operand::Register(dest_reg),
il::Operand::Register(src_reg),
) = &input[next_idx].instruction
{
if *src_reg == def_reg {
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?
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
if let Some(redef) = get_destination_reg(&node.instruction)
&& redef == temp_reg
{
break;
}
// Function calls (jal) clobber the return register (r15)
// So if we're tracking r15 and hit a function call, the old value is dead
if matches!(node.instruction, Instruction::JumpAndLink(_)) && temp_reg == 15 {
break;
}
// Labels are just markers - they don't affect register liveness
// But backward jumps create loops we need to analyze carefully
let jump_target = match &node.instruction {
Instruction::Jump(Operand::Label(target)) => Some(target.as_ref()),
Instruction::BranchEq(_, _, Operand::Label(target))
| Instruction::BranchNe(_, _, Operand::Label(target))
| Instruction::BranchGt(_, _, Operand::Label(target))
| Instruction::BranchLt(_, _, Operand::Label(target))
| Instruction::BranchGe(_, _, Operand::Label(target))
| Instruction::BranchLe(_, _, Operand::Label(target))
| Instruction::BranchEqZero(_, Operand::Label(target))
| Instruction::BranchNeZero(_, Operand::Label(target)) => Some(target.as_ref()),
_ => None,
};
if let Some(target) = jump_target {
// Check if this is a backward jump (target appears before current position)
if let Some(&target_pos) = label_positions.get(target) {
if target_pos < i {
// Backward jump - could loop back, register might be live
temp_is_dead = false;
break;
}
// Forward jump is OK - doesn't affect liveness before it
}
}
}
if temp_is_dead {
// Safety check: ensure final_reg is not used as an operand in the current instruction.
// This prevents generating invalid instructions like `sub r5 r0 r5` (read and write same register).
if !reg_is_read(&input[i].instruction, final_reg) {
// Rewrite to use final destination directly
if let Some(new_instr) = set_destination_reg(&input[i].instruction, final_reg) {
input[i].instruction = new_instr;
input.remove(next_idx);
changed = true;
continue;
}
}
}
}
i += 1;
}
(input, changed)
}
#[cfg(test)]
mod tests {
use super::*;
use il::{Instruction, InstructionNode, Operand};
#[test]
fn test_forward_simple_move() {
let input = vec![
InstructionNode::new(
Instruction::Add(
Operand::Register(1),
Operand::Register(2),
Operand::Register(3),
),
None,
),
InstructionNode::new(
Instruction::Move(Operand::Register(5), Operand::Register(1)),
None,
),
];
let (output, changed) = register_forwarding(input);
assert!(changed);
assert_eq!(output.len(), 1);
assert!(matches!(
output[0].instruction,
Instruction::Add(Operand::Register(5), _, _)
));
}
}

View File

@@ -0,0 +1,63 @@
use il::{Instruction, InstructionNode, Operand};
use rust_decimal::Decimal;
/// Pass: Strength Reduction
/// Replaces expensive operations with cheaper equivalents.
/// Example: x * 2 -> add x x x (addition is typically faster than multiplication)
pub fn strength_reduction<'a>(
input: Vec<InstructionNode<'a>>,
) -> (Vec<InstructionNode<'a>>, bool) {
let mut output = Vec::with_capacity(input.len());
let mut changed = false;
for mut node in input {
let reduced = match &node.instruction {
// x * 2 = x + x
Instruction::Mul(dst, a, Operand::Number(n)) if *n == Decimal::from(2) => {
Some(Instruction::Add(dst.clone(), a.clone(), a.clone()))
}
Instruction::Mul(dst, Operand::Number(n), b) if *n == Decimal::from(2) => {
Some(Instruction::Add(dst.clone(), b.clone(), b.clone()))
}
// Future: Could add power-of-2 optimizations using bit shifts if IC10 supports them
// x * 4 = (x + x) + (x + x) or x << 2
// x / 2 = x >> 1
_ => None,
};
if let Some(new) = reduced {
node.instruction = new;
changed = true;
}
output.push(node);
}
(output, changed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mul_two_to_add() {
let input = vec![InstructionNode::new(
Instruction::Mul(
Operand::Register(1),
Operand::Register(2),
Operand::Number(Decimal::from(2)),
),
None,
)];
let (output, changed) = strength_reduction(input);
assert!(changed);
assert!(matches!(
output[0].instruction,
Instruction::Add(Operand::Register(1), Operand::Register(2), Operand::Register(2))
));
}
}

View File

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

View File

@@ -4,6 +4,7 @@ mod test;
pub mod tree_node; pub mod tree_node;
use crate::sys_call::{Math, System}; use crate::sys_call::{Math, System};
use helpers::Span;
use std::{borrow::Cow, io::SeekFrom}; use std::{borrow::Cow, io::SeekFrom};
use sys_call::SysCall; use sys_call::SysCall;
use thiserror::Error; use thiserror::Error;
@@ -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) => { TokenType::Keyword(Keyword::Device) => {
let spanned_dev = self.spanned(|p| p.device())?; let spanned_dev = self.spanned(|p| p.device())?;
@@ -560,9 +567,7 @@ impl<'a> Parser<'a> {
}) })
} }
TokenType::Symbol(Symbol::LParen) => { TokenType::Symbol(Symbol::LParen) => self.parenthesized_or_tuple()?,
self.spanned(|p| p.priority())?.node.map(|node| *node)
}
TokenType::Symbol(Symbol::Minus) => { TokenType::Symbol(Symbol::Minus) => {
let start_span = self.current_span(); let start_span = self.current_span();
@@ -641,8 +646,8 @@ impl<'a> Parser<'a> {
} }
} }
TokenType::Symbol(Symbol::LParen) => *self TokenType::Symbol(Symbol::LParen) => *self
.spanned(|p| p.priority())? .parenthesized_or_tuple()?
.node .map(Box::new)
.ok_or(Error::UnexpectedEOF)?, .ok_or(Error::UnexpectedEOF)?,
TokenType::Identifier(ref id) if SysCall::is_syscall(id) => { TokenType::Identifier(ref id) if SysCall::is_syscall(id) => {
@@ -773,7 +778,8 @@ impl<'a> Parser<'a> {
| Expression::Ternary(_) | Expression::Ternary(_)
| Expression::Negation(_) | Expression::Negation(_)
| Expression::MemberAccess(_) | Expression::MemberAccess(_)
| Expression::MethodCall(_) => {} | Expression::MethodCall(_)
| Expression::Tuple(_) => {}
_ => { _ => {
return Err(Error::InvalidSyntax( return Err(Error::InvalidSyntax(
self.current_span(), self.current_span(),
@@ -1080,19 +1086,39 @@ impl<'a> Parser<'a> {
end_col: right.span.end_col, end_col: right.span.end_col,
}; };
expressions.insert( // Check if the left side is a tuple, and if so, create a TupleAssignment
i, let node = if let Expression::Tuple(tuple_expr) = &left.node {
Spanned { // 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, span,
node: Expression::Assignment(Spanned { node: TupleAssignmentExpression {
span, names,
node: AssignmentExpression { value: boxed!(right),
assignee: boxed!(left), },
expression: 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)); operators.retain(|symbol| !matches!(symbol, Symbol::Assign));
@@ -1116,8 +1142,12 @@ impl<'a> Parser<'a> {
expressions.pop().ok_or(Error::UnexpectedEOF) 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)?; let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?;
if !token_matches!(current_token, TokenType::Symbol(Symbol::LParen)) { if !token_matches!(current_token, TokenType::Symbol(Symbol::LParen)) {
return Err(Error::UnexpectedToken( return Err(Error::UnexpectedToken(
self.current_span(), self.current_span(),
@@ -1126,17 +1156,112 @@ impl<'a> Parser<'a> {
} }
self.assign_next()?; self.assign_next()?;
let expression = self.expression()?.ok_or(Error::UnexpectedEOF)?;
let current_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?; // Handle empty tuple '()'
if !token_matches!(current_token, TokenType::Symbol(Symbol::RParen)) { if self_matches_peek!(self, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken( self.assign_next()?;
Self::token_to_span(&current_token), let end_span = self.current_span();
current_token, 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>> { fn invocation(&mut self) -> Result<InvocationExpression<'a>, Error<'a>> {
@@ -1833,20 +1958,8 @@ impl<'a> Parser<'a> {
let mut args = args!(3); let mut args = args!(3);
let next = args.next(); let next = args.next();
let dev_name = literal_or_variable!(next); let dev_name = literal_or_variable!(next);
let next = args.next(); let slot_index = args.next().ok_or(Error::UnexpectedEOF)?;
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 next = args.next(); let next = args.next();
let slot_logic = get_arg!(Literal, literal_or_variable!(next)); let slot_logic = get_arg!(Literal, literal_or_variable!(next));
if !matches!( if !matches!(
@@ -1863,27 +1976,17 @@ impl<'a> Parser<'a> {
} }
Ok(SysCall::System(System::LoadSlot( Ok(SysCall::System(System::LoadSlot(
dev_name, slot_index, slot_logic, dev_name,
boxed!(slot_index),
slot_logic,
))) )))
} }
"setSlot" | "ss" => { "setSlot" | "ss" => {
let mut args = args!(4); let mut args = args!(4);
let next = args.next(); let next = args.next();
let dev_name = literal_or_variable!(next); let dev_name = literal_or_variable!(next);
let next = args.next(); let slot_index = args.next().ok_or(Error::UnexpectedEOF)?;
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 next = args.next(); let next = args.next();
let slot_logic = get_arg!(Literal, literal_or_variable!(next)); let slot_logic = get_arg!(Literal, literal_or_variable!(next));
if !matches!( if !matches!(
@@ -1903,9 +2006,23 @@ impl<'a> Parser<'a> {
Ok(SysCall::System(System::SetSlot( Ok(SysCall::System(System::SetSlot(
dev_name, dev_name,
slot_index, boxed!(slot_index),
slot_logic, 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");` /// `let isOccupied = ls(deviceHash, 2, "Occupied");`
LoadSlot( LoadSlot(
Spanned<LiteralOrVariable<'a>>, Spanned<LiteralOrVariable<'a>>,
Spanned<Literal<'a>>, Box<Spanned<Expression<'a>>>,
Spanned<Literal<'a>> Spanned<Literal<'a>>
), ),
/// Stores a value of LogicType on a device by the index value /// Stores a value of LogicType on a device by the index value
@@ -234,7 +234,19 @@ documented! {
/// `ss(deviceHash, 0, "Open", true);` /// `ss(deviceHash, 0, "Open", true);`
SetSlot( SetSlot(
Spanned<LiteralOrVariable<'a>>, Spanned<LiteralOrVariable<'a>>,
Box<Spanned<Expression<'a>>>,
Spanned<Literal<'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>>, Spanned<Literal<'a>>,
Box<Spanned<Expression<'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::LoadSlot(a, b, c) => write!(f, "loadSlot({}, {}, {})", a, b, c),
System::SetSlot(a, b, c, d) => write!(f, "setSlot({}, {}, {}, {})", a, b, c, d), 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 crate::Parser;
use pretty_assertions::assert_eq;
use tokenizer::Tokenizer;
#[test] #[test]
fn test_block() -> anyhow::Result<()> { fn test_block() -> anyhow::Result<()> {

View File

@@ -54,10 +54,7 @@ fn test_const_declaration() -> Result<()> {
let tokenizer = Tokenizer::from(input); let tokenizer = Tokenizer::from(input);
let mut parser = Parser::new(tokenizer); let mut parser = Parser::new(tokenizer);
assert_eq!( assert_eq!("(const item = 20c)", parser.parse()?.unwrap().to_string());
"(const item = 293.15)",
parser.parse()?.unwrap().to_string()
);
assert_eq!( assert_eq!(
"(const decimal = 200.15)", "(const decimal = 200.15)",
@@ -115,7 +112,7 @@ fn test_function_invocation() -> Result<()> {
#[test] #[test]
fn test_priority_expression() -> Result<()> { fn test_priority_expression() -> Result<()> {
let input = r#" let input = r#"
let x = (4); let x = (4 + 3);
"#; "#;
let tokenizer = Tokenizer::from(input); let tokenizer = Tokenizer::from(input);
@@ -123,7 +120,7 @@ fn test_priority_expression() -> Result<()> {
let expression = parser.parse()?.unwrap(); let expression = parser.parse()?.unwrap();
assert_eq!("(let x = 4)", expression.to_string()); assert_eq!("(let x = ((4 + 3)))", expression.to_string());
Ok(()) Ok(())
} }
@@ -140,7 +137,7 @@ fn test_binary_expression() -> Result<()> {
assert_eq!("(((45 * 2) - (15 / 5)) + (5 ** 2))", expr.to_string()); assert_eq!("(((45 * 2) - (15 / 5)) + (5 ** 2))", expr.to_string());
let expr = parser!("(5 - 2) * 10;").parse()?.unwrap(); 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(()) Ok(())
} }
@@ -173,7 +170,7 @@ fn test_ternary_expression() -> Result<()> {
fn test_complex_binary_with_ternary() -> Result<()> { fn test_complex_binary_with_ternary() -> Result<()> {
let expr = parser!("let i = (x ? 1 : 3) * 2;").parse()?.unwrap(); let expr = parser!("let i = (x ? 1 : 3) * 2;").parse()?.unwrap();
assert_eq!("(let i = ((x ? 1 : 3) * 2))", expr.to_string()); assert_eq!("(let i = (((x ? 1 : 3)) * 2))", expr.to_string());
Ok(()) Ok(())
} }
@@ -194,3 +191,99 @@ fn test_nested_ternary_right_associativity() -> Result<()> {
assert_eq!("(let i = (a ? b : (c ? d : e)))", expr.to_string()); assert_eq!("(let i = (a ? b : (c ? d : e)))", expr.to_string());
Ok(()) 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 super::sys_call::SysCall;
use crate::sys_call; use crate::sys_call;
use helpers::Span;
use safer_ffi::prelude::*;
use std::{borrow::Cow, ops::Deref}; use std::{borrow::Cow, ops::Deref};
use tokenizer::token::Number; 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)] #[derive(Debug, PartialEq, Eq)]
pub struct IfExpression<'a> { pub struct IfExpression<'a> {
pub condition: Box<Spanned<Expression<'a>>>, pub condition: Box<Spanned<Expression<'a>>>,
@@ -300,44 +338,6 @@ impl<'a> std::fmt::Display for WhileExpression<'a> {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
}
impl From<Span> for lsp_types::Range {
fn from(value: Span) -> Self {
Self {
start: lsp_types::Position {
line: value.start_line as u32,
character: value.start_col as u32,
},
end: lsp_types::Position {
line: value.end_line as u32,
character: value.end_col as u32,
},
}
}
}
impl From<&Span> for lsp_types::Range {
fn from(value: &Span) -> Self {
Self {
start: lsp_types::Position {
line: value.start_line as u32,
character: value.start_col as u32,
},
end: lsp_types::Position {
line: value.end_line as u32,
character: value.end_col as u32,
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Spanned<T> { pub struct Spanned<T> {
pub span: Span, pub span: Span,
@@ -384,6 +384,9 @@ pub enum Expression<'a> {
Return(Option<Box<Spanned<Expression<'a>>>>), Return(Option<Box<Spanned<Expression<'a>>>>),
Syscall(Spanned<SysCall<'a>>), Syscall(Spanned<SysCall<'a>>),
Ternary(Spanned<TernaryExpression<'a>>), Ternary(Spanned<TernaryExpression<'a>>),
Tuple(Spanned<Vec<Spanned<Expression<'a>>>>),
TupleAssignment(Spanned<TupleAssignmentExpression<'a>>),
TupleDeclaration(Spanned<TupleDeclarationExpression<'a>>),
Variable(Spanned<Cow<'a, str>>), Variable(Spanned<Cow<'a, str>>),
While(Spanned<WhileExpression<'a>>), While(Spanned<WhileExpression<'a>>),
} }
@@ -420,8 +423,20 @@ impl<'a> std::fmt::Display for Expression<'a> {
), ),
Expression::Syscall(e) => write!(f, "{}", e), Expression::Syscall(e) => write!(f, "{}", e),
Expression::Ternary(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::Variable(id) => write!(f, "{}", id),
Expression::While(e) => write!(f, "{}", e), 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 { macro_rules! symbol {
($var:ident) => { ($var:ident) => {
|_| Symbol::$var |_| Symbol::$var
@@ -157,7 +115,7 @@ macro_rules! keyword {
} }
#[derive(Debug, PartialEq, Hash, Eq, Clone, Logos)] #[derive(Debug, PartialEq, Hash, Eq, Clone, Logos)]
#[logos(skip r"[ \t\f]+")] #[logos(skip r"[ \r\t\f]+")]
#[logos(extras = Extras)] #[logos(extras = Extras)]
#[logos(error(LexError, LexError::from_lexer))] #[logos(error(LexError, LexError::from_lexer))]
pub enum TokenType<'a> { pub enum TokenType<'a> {
@@ -280,30 +238,27 @@ fn parse_number<'a>(lexer: &mut Lexer<'a, TokenType<'a>>) -> Result<Number, LexE
span.end -= lexer.extras.line_start_index; span.end -= lexer.extras.line_start_index;
span.start -= lexer.extras.line_start_index; span.start -= lexer.extras.line_start_index;
let num = if clean_str.contains('.') { let unit = match suffix {
Number::Decimal( Some('c') => Unit::Celsius,
Some('f') => Unit::Fahrenheit,
Some('k') => Unit::Kelvin,
_ => Unit::None,
};
if clean_str.contains('.') {
Ok(Number::Decimal(
clean_str clean_str
.parse::<Decimal>() .parse::<Decimal>()
.map_err(|_| LexError::NumberParse(line, span, slice.to_string()))?, .map_err(|_| LexError::NumberParse(line, span, slice.to_string()))?,
) unit,
))
} else { } else {
Number::Integer( Ok(Number::Integer(
clean_str clean_str
.parse::<i128>() .parse::<i128>()
.map_err(|_| LexError::NumberParse(line, span, slice.to_string()))?, .map_err(|_| LexError::NumberParse(line, span, slice.to_string()))?,
) unit,
}; ))
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)
} }
} }
@@ -395,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)] #[derive(Debug, PartialEq, Hash, Eq, Clone, Copy)]
pub enum Number { pub enum Number {
/// Represents an integer number /// Represents an integer number
Integer(i128), Integer(i128, Unit),
/// Represents a decimal type number with a precision of 64 bits /// 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 { impl From<Number> for Decimal {
fn from(value: Number) -> Self { fn from(value: Number) -> Self {
match value { let (val, unit) = match value {
Number::Decimal(d) => d, Number::Decimal(d, u) => (d, u),
Number::Integer(i) => Decimal::from(i), 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)
}
} }
} }
} }
@@ -417,22 +408,48 @@ impl std::ops::Neg for Number {
fn neg(self) -> Self::Output { fn neg(self) -> Self::Output {
match self { match self {
Self::Integer(i) => Self::Integer(-i), Self::Integer(i, u) => Self::Integer(-i, u),
Self::Decimal(d) => Self::Decimal(-d), 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 { impl std::ops::Add for Number {
type Output = Number; type Output = Number;
fn add(self, rhs: Self) -> Self::Output { fn add(self, rhs: Self) -> Self::Output {
match (self, rhs) { // If we can determine a common target unit (e.g. C + C = C, or C + Scalar = C),
(Self::Integer(l), Self::Integer(r)) => Number::Integer(l + r), // we preserve that unit. Otherwise, we convert to Kelvin (Decimal) and return Unit::None.
(Self::Decimal(l), Self::Decimal(r)) => Number::Decimal(l + r), if let Some(target_unit) = determine_target_unit(self.unit(), rhs.unit()) {
(Self::Integer(l), Self::Decimal(r)) => Number::Decimal(Decimal::from(l) + r), return match (self, rhs) {
(Self::Decimal(l), Self::Integer(r)) => Number::Decimal(l + Decimal::from(r)), (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)
} }
} }
@@ -440,12 +457,22 @@ impl std::ops::Sub for Number {
type Output = Number; type Output = Number;
fn sub(self, rhs: Self) -> Self::Output { fn sub(self, rhs: Self) -> Self::Output {
match (self, rhs) { if let Some(target_unit) = determine_target_unit(self.unit(), rhs.unit()) {
(Self::Integer(l), Self::Integer(r)) => Self::Integer(l - r), return match (self, rhs) {
(Self::Decimal(l), Self::Integer(r)) => Self::Decimal(l - Decimal::from(r)), (Self::Integer(l, _), Self::Integer(r, _)) => Number::Integer(l - r, target_unit),
(Self::Integer(l), Self::Decimal(r)) => Self::Decimal(Decimal::from(l) - r), (Self::Decimal(l, _), Self::Decimal(r, _)) => Number::Decimal(l - r, target_unit),
(Self::Decimal(l), Self::Decimal(r)) => Self::Decimal(l - r), (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)
} }
} }
@@ -453,12 +480,26 @@ impl std::ops::Mul for Number {
type Output = Number; type Output = Number;
fn mul(self, rhs: Self) -> Self::Output { fn mul(self, rhs: Self) -> Self::Output {
match (self, rhs) { if let Some(target_unit) = determine_target_unit(self.unit(), rhs.unit()) {
(Number::Integer(l), Number::Integer(r)) => Number::Integer(l * r), return match (self, rhs) {
(Number::Integer(l), Number::Decimal(r)) => Number::Decimal(Decimal::from(l) * r), (Number::Integer(l, _), Number::Integer(r, _)) => {
(Number::Decimal(l), Number::Integer(r)) => Number::Decimal(l * Decimal::from(r)), Number::Integer(l * r, target_unit)
(Number::Decimal(l), Number::Decimal(r)) => Number::Decimal(l * r), }
(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)
} }
} }
@@ -466,7 +507,22 @@ impl std::ops::Div for Number {
type Output = Number; type Output = Number;
fn div(self, rhs: Self) -> Self::Output { 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)
} }
} }
@@ -474,15 +530,36 @@ impl std::ops::Rem for Number {
type Output = Number; type Output = Number;
fn rem(self, rhs: Self) -> Self::Output { 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 { impl std::fmt::Display for Number {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { let (val, unit) = match self {
Number::Integer(i) => write!(f, "{}", i), Number::Integer(i, u) => (i.to_string(), u),
Number::Decimal(d) => write!(f, "{}", d), 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),
} }
} }
} }
@@ -765,3 +842,21 @@ documented! {
While, 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 compiler::{CompilationResult, Compiler};
use helpers::Documentation; use helpers::{Documentation, Span};
use parser::{sys_call::SysCall, Parser}; use parser::{sys_call::SysCall, Parser};
use safer_ffi::prelude::*; use safer_ffi::prelude::*;
use std::io::BufWriter; use std::io::BufWriter;
@@ -8,6 +8,20 @@ use tokenizer::{
Tokenizer, Tokenizer,
}; };
#[derive_ReprC]
#[repr(C)]
pub struct FfiSourceMapEntry {
pub line_number: u32,
pub span: FfiRange,
}
#[derive_ReprC]
#[repr(C)]
pub struct FfiCompilationResult {
pub output_code: safer_ffi::String,
pub source_map: safer_ffi::Vec<FfiSourceMapEntry>,
}
#[derive_ReprC] #[derive_ReprC]
#[repr(C)] #[repr(C)]
pub struct FfiToken { pub struct FfiToken {
@@ -34,6 +48,17 @@ pub struct FfiDocumentedItem {
docs: safer_ffi::String, docs: safer_ffi::String,
} }
impl From<Span> for FfiRange {
fn from(value: Span) -> Self {
Self {
start_line: value.start_line as u32,
end_line: value.end_line as u32,
start_col: value.start_col as u32,
end_col: value.end_col as u32,
}
}
}
impl From<lsp_types::Range> for FfiRange { impl From<lsp_types::Range> for FfiRange {
fn from(value: lsp_types::Range) -> Self { fn from(value: lsp_types::Range) -> Self {
Self { Self {
@@ -69,6 +94,11 @@ impl From<lsp_types::Diagnostic> for FfiDiagnostic {
} }
} }
#[ffi_export]
pub fn free_ffi_compilation_result(input: FfiCompilationResult) {
drop(input)
}
#[ffi_export] #[ffi_export]
pub fn free_ffi_token_vec(v: safer_ffi::Vec<FfiToken>) { pub fn free_ffi_token_vec(v: safer_ffi::Vec<FfiToken>) {
drop(v) drop(v)
@@ -94,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 /// This should result in the ability to compile many times without triggering frame drops
/// from the GC from a `GetBytes()` call on a string in C#. /// from the GC from a `GetBytes()` call on a string in C#.
#[ffi_export] #[ffi_export]
pub fn compile_from_string(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::String { pub fn compile_from_string(input: safer_ffi::slice::Ref<'_, u16>) -> FfiCompilationResult {
let res = std::panic::catch_unwind(|| { let res = std::panic::catch_unwind(|| {
let input = String::from_utf16_lossy(input.as_slice()); let input = String::from_utf16_lossy(input.as_slice());
let mut writer = BufWriter::new(Vec::new());
let tokenizer = Tokenizer::from(input.as_str()); let tokenizer = Tokenizer::from(input.as_str());
let parser = Parser::new(tokenizer); let parser = Parser::new(tokenizer);
let compiler = Compiler::new(parser, &mut writer, None); let compiler = Compiler::new(parser, None);
if !compiler.compile().is_empty() { let res = compiler.compile();
return safer_ffi::String::EMPTY;
if !res.errors.is_empty() {
return (safer_ffi::String::EMPTY, res.instructions.source_map());
} }
let mut writer = BufWriter::new(Vec::new());
// writing into a Vec<u8>. This should not fail.
let optimized = optimizer::optimize(res.instructions);
let map = optimized.source_map();
_ = optimized.write(&mut writer);
let Ok(compiled_vec) = writer.into_inner() else { let Ok(compiled_vec) = writer.into_inner() else {
return safer_ffi::String::EMPTY; return (safer_ffi::String::EMPTY, map);
}; };
// Safety: I know the compiler only outputs valid utf8 // Safety: I know the compiler only outputs valid utf8
safer_ffi::String::from(unsafe { String::from_utf8_unchecked(compiled_vec) }) (
safer_ffi::String::from(unsafe { String::from_utf8_unchecked(compiled_vec) }),
map,
)
}); });
res.unwrap_or("".into()) if let Ok((res_str, source_map)) = res {
FfiCompilationResult {
source_map: source_map
.into_iter()
.map(|(line_num, span)| FfiSourceMapEntry {
span: span.into(),
line_number: line_num as u32,
})
.collect::<Vec<_>>()
.into(),
output_code: res_str,
}
} else {
FfiCompilationResult {
output_code: "".into(),
source_map: vec![].into(),
}
}
} }
#[ffi_export] #[ffi_export]
@@ -180,11 +238,12 @@ pub fn diagnose_source(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec<
let res = std::panic::catch_unwind(|| { let res = std::panic::catch_unwind(|| {
let input = String::from_utf16_lossy(input.as_slice()); let input = String::from_utf16_lossy(input.as_slice());
let mut writer = BufWriter::new(Vec::new());
let tokenizer = Tokenizer::from(input.as_str()); 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()); let mut result_vec: Vec<FfiDiagnostic> = Vec::with_capacity(diagnosis.len());

View File

@@ -1,7 +1,7 @@
#![allow(clippy::result_large_err)] #![allow(clippy::result_large_err)]
use clap::Parser; use clap::Parser;
use compiler::Compiler; use compiler::{CompilationResult, Compiler};
use parser::Parser as ASTParser; use parser::Parser as ASTParser;
use std::{ use std::{
fs::File, fs::File,
@@ -53,6 +53,9 @@ struct Args {
/// The output file for the compiled program. If not set, output will go to stdout. /// The output file for the compiled program. If not set, output will go to stdout.
#[arg(short, long)] #[arg(short, long)]
output_file: Option<PathBuf>, output_file: Option<PathBuf>,
/// Should Slang attempt to optimize the output?
#[arg(short = 'z', long)]
optimize: bool,
} }
fn run_logic<'a>() -> Result<(), Error<'a>> { 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())), 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() { if !errors.is_empty() {
let mut std_error = stderr(); 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()?; writer.flush()?;
Ok(()) Ok(())