118 Commits

Author SHA1 Message Date
b8521917b8 First pass getting user documentation in the IDE
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 38s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2026-01-02 19:14:58 -07:00
4ff0ff1b66 Track various symbols in the parse stage of Slang 2026-01-02 17:06:43 -07:00
6dc4342ac3 First pass getting deeper LSP into IDE 2026-01-02 16:57:06 -07:00
2070c2e4ca CLI error handling
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-02 16:44:38 -07:00
4c704b8960 Attempt to fold constants when folding expressions
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-02 03:06:37 -07:00
3c7300d2e1 Improve unexpectedEOF tracking
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 16:51:35 -07:00
647c3d29d5 Improve binary folding 2026-01-01 15:57:35 -07:00
e3a38ec110 Remove MD file 2026-01-01 14:01:34 -07:00
1f98ab8d75 Removed unneeded parenthesis 2026-01-01 14:01:14 -07:00
76c5df5dc2 Bitwise folding with hash() as well 2026-01-01 14:00:10 -07:00
0999ae8aed Array indexing support first pass 2026-01-01 13:46:04 -07:00
072a6b9ea6 Merge master 2026-01-01 12:50:02 -07:00
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
f5a28dfd6d restructure integration tests 2026-01-01 03:33:54 -07:00
9966009500 update tests 2026-01-01 03:25:53 -07:00
bc7c77846f Added tokenizer tests for new literal types 2026-01-01 03:18:51 -07:00
76add65235 Add support for binary, hex, and octal literals 2026-01-01 03:12:25 -07:00
e56414c251 First pass with bitwise 2026-01-01 03:05:21 -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
119 changed files with 15694 additions and 1821 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,105 @@
# 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] [0.2.4]
- Groundwork laid to collect and track source maps - Groundwork laid to collect and track source maps

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.4</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

@@ -207,4 +207,34 @@ public static unsafe class SlangExtensions
Ffi.free_docs_vec(vec); Ffi.free_docs_vec(vec);
return toReturn; return toReturn;
} }
public static unsafe List<Symbol> ToList(this Vec_FfiSymbolInfo_t vec)
{
var toReturn = new List<Symbol>((int)vec.len);
var currentPtr = vec.ptr;
for (int i = 0; i < (int)vec.len; i++)
{
var item = currentPtr[i];
toReturn.Add(
new Slang.Symbol
{
Name = item.name.AsString(),
Kind = (SymbolKind)item.kind_data.kind,
Span = new Slang.Range
{
StartLine = item.span.start_line,
StartCol = item.span.start_col,
EndLine = item.span.end_line,
EndCol = item.span.end_col,
},
Description = item.description.AsString(),
}
);
}
return toReturn;
}
} }

View File

@@ -147,6 +147,51 @@ public unsafe partial class Ffi {
slice_ref_uint16_t input); slice_ref_uint16_t input);
} }
[StructLayout(LayoutKind.Sequential, Size = 12)]
public unsafe struct FfiSymbolKindData_t {
public UInt32 kind;
public UInt32 arg_count;
public UInt32 syscall_type;
}
[StructLayout(LayoutKind.Sequential, Size = 80)]
public unsafe struct FfiSymbolInfo_t {
public Vec_uint8_t name;
public FfiSymbolKindData_t kind_data;
public FfiRange_t span;
public Vec_uint8_t description;
}
/// <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_FfiSymbolInfo_t {
public FfiSymbolInfo_t * ptr;
public UIntPtr len;
public UIntPtr cap;
}
[StructLayout(LayoutKind.Sequential, Size = 48)]
public unsafe struct FfiDiagnosticsAndSymbols_t {
public Vec_FfiDiagnostic_t diagnostics;
public Vec_FfiSymbolInfo_t symbols;
}
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
FfiDiagnosticsAndSymbols_t diagnose_source_with_symbols (
slice_ref_uint16_t input);
}
[StructLayout(LayoutKind.Sequential, Size = 48)] [StructLayout(LayoutKind.Sequential, Size = 48)]
public unsafe struct FfiDocumentedItem_t { public unsafe struct FfiDocumentedItem_t {
public Vec_uint8_t item_name; public Vec_uint8_t item_name;
@@ -184,6 +229,12 @@ public unsafe partial class Ffi {
Vec_FfiDiagnostic_t v); Vec_FfiDiagnostic_t v);
} }
public unsafe partial class Ffi {
[DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
void free_ffi_diagnostics_and_symbols (
FfiDiagnosticsAndSymbols_t v);
}
[StructLayout(LayoutKind.Sequential, Size = 64)] [StructLayout(LayoutKind.Sequential, Size = 64)]
public unsafe struct FfiToken_t { public unsafe struct FfiToken_t {
public Vec_uint8_t tooltip; public Vec_uint8_t tooltip;

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
@@ -114,18 +171,40 @@ public class SlangFormatter : ICodeFormatter
return; return;
// Running this potentially CPU intensive work on a background thread. // Running this potentially CPU intensive work on a background thread.
var dict = await Task.Run( var (diagnostics, symbols) = await Task.Run(
() => () =>
{ {
return Marshal return Marshal.DiagnoseSourceWithSymbols(inputSrc);
.DiagnoseSource(inputSrc)
.GroupBy(d => d.Range.StartLine)
.ToDictionary(g => g.Key);
}, },
cancellationToken cancellationToken
); );
ApplyDiagnostics(dict); var dict = diagnostics.GroupBy(d => d.Range.StartLine).ToDictionary(g => g.Key);
ApplyDiagnosticsAndSymbols(dict, symbols);
// 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,8 +213,63 @@ public class SlangFormatter : ICodeFormatter
} }
} }
// This runs on the Main Thread /// <summary>
private void ApplyDiagnostics(Dictionary<uint, IGrouping<uint, Diagnostic>> dict) /// 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 and symbols from the Rust FFI compiler and applies them as semantic tokens to the
/// source in this editor.
/// This runs on the Main Thread
/// </summary>
private void ApplyDiagnosticsAndSymbols(Dictionary<uint, IGrouping<uint, Diagnostic>> dict, List<Symbol> symbols)
{ {
HashSet<uint> linesToRefresh; HashSet<uint> linesToRefresh;
@@ -154,6 +288,12 @@ public class SlangFormatter : ICodeFormatter
{ {
linesToRefresh = new HashSet<uint>(dict.Keys); linesToRefresh = new HashSet<uint>(dict.Keys);
linesToRefresh.UnionWith(_linesWithErrors); linesToRefresh.UnionWith(_linesWithErrors);
// Also add lines with symbols that may have been modified
foreach (var symbol in symbols)
{
linesToRefresh.Add(symbol.Span.StartLine);
}
} }
_lastLineCount = this.Lines.Count; _lastLineCount = this.Lines.Count;
@@ -193,9 +333,49 @@ public class SlangFormatter : ICodeFormatter
} }
} }
// 3. Add symbol tooltips for symbols on this line
foreach (var symbol in symbols)
{
if (symbol.Span.StartLine == lineIndex)
{
var column = (int)symbol.Span.StartCol;
var length = Math.Max(1, (int)(symbol.Span.EndCol - symbol.Span.StartCol));
// If there's already a token at this position (from syntax highlighting), use it
// Otherwise, create a new token for the symbol
if (allTokensDict.ContainsKey(column))
{
// Update existing token with symbol tooltip
var existingToken = allTokensDict[column];
allTokensDict[column] = new SemanticToken(
line: existingToken.Line,
column: existingToken.Column,
length: existingToken.Length,
type: existingToken.Type,
style: existingToken.Style,
data: symbol.Description, // Use symbol description as tooltip
isError: existingToken.IsError
);
}
else
{
// Create new token for symbol
allTokensDict[column] = new SemanticToken(
line: (int)lineIndex,
column,
length,
type: 0,
style: ColorIdentifier,
data: symbol.Description,
isError: false
);
}
}
}
var allTokens = allTokensDict.Values.ToList(); var allTokens = allTokensDict.Values.ToList();
// 3. Update the line (this clears existing tokens and uses the list we just built) // 4. Update the line (this clears existing tokens and uses the list we just built)
line.Update(allTokens); line.Update(allTokens);
ReattachMetadata(line, allTokens); ReattachMetadata(line, allTokens);
@@ -204,6 +384,16 @@ public class SlangFormatter : ICodeFormatter
_linesWithErrors = new HashSet<uint>(dict.Keys); _linesWithErrors = new HashSet<uint>(dict.Keys);
} }
/// <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)
{
ApplyDiagnosticsAndSymbols(dict, new List<Symbol>());
}
// Helper to map SemanticToken data (tooltips/errors) back to the tokens in the line // Helper to map SemanticToken data (tooltips/errors) back to the tokens in the line
private void ReattachMetadata(StyledLine line, List<SemanticToken> semanticTokens) private void ReattachMetadata(StyledLine line, List<SemanticToken> semanticTokens)
{ {

View File

@@ -1,30 +1,24 @@
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
// This Dictionary stores the source maps for the given SLANG_REF, where /// </summary>
// the key is the IC10 line, and the value is a List of Slang ranges where that
// line would have come from
private static Dictionary<Guid, Dictionary<uint, List<Range>>> sourceMaps = new(); private static Dictionary<Guid, Dictionary<uint, List<Range>>> sourceMaps = new();
public static void ClearCache()
{
codeDict.Clear();
}
public static void SetSourceMap(Guid reference, List<SourceMapEntry> sourceMapEntries) public static void SetSourceMap(Guid reference, List<SourceMapEntry> sourceMapEntries)
{ {
var builtDictionary = new Dictionary<uint, List<Range>>(); var builtDictionary = new Dictionary<uint, List<Range>>();
@@ -56,6 +50,11 @@ public static class GlobalCode
return false; return false;
} }
if (!sourceMaps[reference].ContainsKey(icErrorLine))
{
return false;
}
var foundRange = sourceMaps[reference][icErrorLine]; var foundRange = sourceMaps[reference][icErrorLine];
if (foundRange is null) if (foundRange is null)
@@ -67,81 +66,4 @@ public static class GlobalCode
slangSpan = foundRange[0]; slangSpan = foundRange[0];
return true; return true;
} }
public static string GetSource(Guid reference)
{
if (!codeDict.ContainsKey(reference))
{
return string.Empty;
}
return DecodeSource(codeDict[reference]);
}
public static void SetSource(Guid reference, string source)
{
codeDict[reference] = EncodeSource(source);
}
public static string? GetEncoded(Guid reference)
{
if (!codeDict.ContainsKey(reference))
return null;
return codeDict[reference];
}
public static void SetEncoded(Guid reference, string encodedSource)
{
if (codeDict.ContainsKey(reference))
{
codeDict[reference] = encodedSource;
}
else
{
codeDict.Add(reference, encodedSource);
}
}
private static string EncodeSource(string source)
{
if (string.IsNullOrEmpty(source))
{
return "";
}
byte[] bytes = Encoding.UTF8.GetBytes(source);
using (var memoryStream = new MemoryStream())
{
using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress))
{
gzipStream.Write(bytes, 0, bytes.Length);
}
return Convert.ToBase64String(memoryStream.ToArray());
}
}
private static string DecodeSource(string source)
{
if (string.IsNullOrEmpty(source))
{
return "";
}
byte[] compressedBytes = Convert.FromBase64String(source);
using (var memoryStream = new MemoryStream(compressedBytes))
{
using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
{
using (var outputStream = new MemoryStream())
{
gzipStream.CopyTo(outputStream);
return Encoding.UTF8.GetString(outputStream.ToArray());
}
}
}
}
} }

View File

@@ -47,6 +47,33 @@ public struct SourceMapEntry
} }
} }
public struct Symbol
{
public string Name;
public Range Span;
public SymbolKind Kind;
public string Description;
public override string ToString()
{
return $"{Kind}: {Name} at {Span}";
}
}
public enum SymbolKind
{
Function = 0,
Syscall = 1,
Variable = 2,
}
public struct SymbolData
{
public uint Kind;
public uint ArgCount;
public uint SyscallType; // 0=System, 1=Math
}
public static class Marshal public static class Marshal
{ {
private static IntPtr _libraryHandle = IntPtr.Zero; private static IntPtr _libraryHandle = IntPtr.Zero;
@@ -67,7 +94,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)
@@ -91,8 +120,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)
@@ -157,6 +191,59 @@ public static class Marshal
} }
} }
public static unsafe (List<Diagnostic>, List<Symbol>) DiagnoseSourceWithSymbols(string inputString)
{
if (string.IsNullOrEmpty(inputString) || !EnsureLibLoaded())
{
return (new(), new());
}
fixed (char* ptrInput = inputString)
{
var input = new slice_ref_uint16_t
{
ptr = (ushort*)ptrInput,
len = (UIntPtr)inputString.Length,
};
var result = Ffi.diagnose_source_with_symbols(input);
// Convert diagnostics
var diagnostics = result.diagnostics.ToList();
// Convert symbols
var symbols = new List<Symbol>();
var symbolPtr = result.symbols.ptr;
var symbolCount = (int)result.symbols.len;
for (int i = 0; i < symbolCount; i++)
{
var ffiSymbol = symbolPtr[i];
var kind = (SymbolKind)ffiSymbol.kind_data.kind;
// Use the actual description from the FFI (includes doc comments and syscall docs)
var description = ffiSymbol.description.AsString();
symbols.Add(new Symbol
{
Name = ffiSymbol.name.AsString(),
Kind = kind,
Span = new Range(
ffiSymbol.span.start_line,
ffiSymbol.span.start_col,
ffiSymbol.span.end_line,
ffiSymbol.span.end_col
),
Description = description,
});
}
Ffi.free_ffi_diagnostics_and_symbols(result);
return (diagnostics, symbols);
}
}
public static unsafe List<SemanticToken> TokenizeLine(string inputString) public static unsafe List<SemanticToken> TokenizeLine(string inputString)
{ {
if (string.IsNullOrEmpty(inputString) || !EnsureLibLoaded()) if (string.IsNullOrEmpty(inputString) || !EnsureLibLoaded())
@@ -191,9 +278,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!"
@@ -201,18 +288,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,355 +1,11 @@
namespace Slang; namespace Slang;
using System;
using System.Runtime.CompilerServices;
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;
class LineErrorData
{
public AsciiString SourceRef;
public uint IC10ErrorSource;
public string SlangErrorReference;
public Range SlangErrorSpan;
public LineErrorData(
AsciiString sourceRef,
uint ic10ErrorSource,
string slangErrorRef,
Range slangErrorSpan
)
{
this.SourceRef = sourceRef;
this.IC10ErrorSource = ic10ErrorSource;
this.SlangErrorReference = slangErrorRef;
this.SlangErrorSpan = slangErrorSpan;
}
}
[HarmonyPatch] [HarmonyPatch]
public static class SlangPatches public static class SlangPatches
{ {
private static ProgrammableChipMotherboard? _currentlyEditingMotherboard;
private static AsciiString? _motherboardCachedCode;
private static Guid? _currentlyEditingGuid;
private static ConditionalWeakTable<ProgrammableChip, LineErrorData> _errorReferenceTable =
new();
[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 var compiled, out var sourceMap)
|| string.IsNullOrEmpty(compiled)
)
{
return;
}
var thisRef = _currentlyEditingGuid ?? Guid.NewGuid();
// Ensure we cache this compiled code for later retreival.
GlobalCode.SetSource(thisRef, result);
GlobalCode.SetSourceMap(thisRef, sourceMap);
_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(ProgrammableChip),
nameof(ProgrammableChip.ErrorLineNumberString),
MethodType.Getter
)]
[HarmonyPostfix]
public static void pgc_ErrorLineNumberString(ProgrammableChip __instance, ref string __result)
{
if (
String.IsNullOrEmpty(__result)
|| !uint.TryParse(__result.Trim(), out var ic10ErrorLineNumber)
)
{
return;
}
var sourceAscii = __instance.GetSourceCode();
if (_errorReferenceTable.TryGetValue(__instance, out var cache))
{
if (cache.SourceRef.Equals(sourceAscii) && cache.IC10ErrorSource == ic10ErrorLineNumber)
{
__result = cache.SlangErrorReference;
return;
}
}
var source = System.Text.Encoding.UTF8.GetString(
System.Text.Encoding.ASCII.GetBytes(__instance.GetSourceCode())
);
var slangIndex = source.LastIndexOf(GlobalCode.SLANG_REF);
if (
slangIndex < 0
|| !Guid.TryParse(
source
.Substring(
source.LastIndexOf(GlobalCode.SLANG_REF) + GlobalCode.SLANG_REF.Length
)
.Trim(),
out var slangGuid
)
|| !GlobalCode.GetSlangErrorLineFromICError(
slangGuid,
ic10ErrorLineNumber,
out var slangErrorLineNumber,
out var slangSpan
)
)
{
return;
}
L.Warning($"IC error at: {__result} -- Slang source error line: {slangErrorLineNumber}");
__result = slangErrorLineNumber.ToString();
_errorReferenceTable.Remove(__instance);
_errorReferenceTable.Add(
__instance,
new LineErrorData(
sourceAscii,
ic10ErrorLineNumber,
slangErrorLineNumber.ToString(),
slangSpan
)
);
}
[HarmonyPatch(
typeof(ProgrammableChip),
nameof(ProgrammableChip.SetSourceCode),
new Type[] { typeof(string) }
)]
[HarmonyPostfix]
public static void pgc_SetSourceCode_string(ProgrammableChip __instance, string sourceCode)
{
_errorReferenceTable.Remove(__instance);
}
[HarmonyPatch(
typeof(ProgrammableChip),
nameof(ProgrammableChip.SetSourceCode),
new Type[] { typeof(string), typeof(ICircuitHolder) }
)]
[HarmonyPostfix]
public static void pgc_SetSourceCode_string_parent(
ProgrammableChip __instance,
string sourceCode,
ICircuitHolder parent
)
{
_errorReferenceTable.Remove(__instance);
}
[HarmonyPatch(
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.2.4"; 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.4</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

315
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"
@@ -167,14 +173,14 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.111", "syn 2.0.112",
] ]
[[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"
@@ -218,9 +224,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.53" version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -228,9 +234,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.53" version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -247,7 +253,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.111", "syn 2.0.112",
] ]
[[package]] [[package]]
@@ -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"
@@ -435,9 +523,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
@@ -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"
@@ -477,7 +571,7 @@ dependencies = [
"regex-automata", "regex-automata",
"regex-syntax", "regex-syntax",
"rustc_version", "rustc_version",
"syn 2.0.111", "syn 2.0.112",
] ]
[[package]] [[package]]
@@ -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"
@@ -622,9 +726,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.103" version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -658,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"
@@ -691,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]]
@@ -780,18 +890,25 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "safer-ffi" name = "safer-ffi"
version = "0.1.13" version = "0.1.13"
@@ -869,20 +986,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.111", "syn 2.0.112",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.145" version = "1.0.148"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
"ryu",
"serde", "serde",
"serde_core", "serde_core",
"zmij",
] ]
[[package]] [[package]]
@@ -893,7 +1010,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.111", "syn 2.0.112",
] ]
[[package]] [[package]]
@@ -908,15 +1025,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.4" 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",
@@ -978,9 +1102,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.111" version = "2.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -993,6 +1117,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"
@@ -1010,7 +1147,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.111", "syn 2.0.112",
] ]
[[package]] [[package]]
@@ -1042,18 +1179,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.7.3" version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
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",
@@ -1063,9 +1200,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.4" version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
dependencies = [ dependencies = [
"winnow", "winnow",
] ]
@@ -1119,6 +1256,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"
@@ -1151,7 +1297,7 @@ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.111", "syn 2.0.112",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -1170,6 +1316,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"
@@ -1179,6 +1334,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"
@@ -1188,6 +1407,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"
@@ -1240,5 +1465,11 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.111", "syn 2.0.112",
] ]
[[package]]
name = "zmij"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "slang" name = "slang"
version = "0.2.4" 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

@@ -1,6 +1,8 @@
pub mod symbols;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
mod v1; mod v1;
mod variable_manager; mod variable_manager;
pub use symbols::{CompilationMetadata, SymbolInfo, SymbolKind, SyscallType};
pub use v1::{CompilationResult, Compiler, CompilerConfig, Error}; pub use v1::{CompilationResult, Compiler, CompilerConfig, Error};

View File

@@ -0,0 +1,343 @@
use helpers::Span;
use std::borrow::Cow;
/// Represents a symbol (function, syscall, variable, etc.) that can be referenced in code.
/// Designed to be LSP-compatible for easy integration with language servers.
#[derive(Debug, Clone)]
pub struct SymbolInfo<'a> {
/// The name of the symbol
pub name: Cow<'a, str>,
/// The kind of symbol and associated metadata
pub kind: SymbolKind<'a>,
/// The source location of this symbol (for IDE features)
pub span: Option<Span>,
/// Optional description for tooltips and documentation
pub description: Option<Cow<'a, str>>,
}
impl<'a> SymbolInfo<'a> {
/// Converts to an LSP SymbolInformation for protocol compatibility.
pub fn to_lsp_symbol_information(&self, uri: lsp_types::Uri) -> lsp_types::SymbolInformation {
lsp_types::SymbolInformation {
name: self.name.to_string(),
kind: self.kind.to_lsp_symbol_kind(),
#[allow(deprecated)]
deprecated: None,
location: lsp_types::Location {
uri,
range: self.span.as_ref().map(|s| (*s).into()).unwrap_or_default(),
},
container_name: None,
tags: None,
}
}
/// Converts to an LSP CompletionItem for autocomplete.
pub fn to_lsp_completion_item(&self) -> lsp_types::CompletionItem {
lsp_types::CompletionItem {
label: self.name.to_string(),
kind: Some(self.kind.to_lsp_completion_kind()),
documentation: self
.description
.as_ref()
.map(|d| lsp_types::Documentation::String(d.to_string())),
detail: Some(self.kind.detail_string()),
..Default::default()
}
}
}
/// Discriminates between different kinds of symbols.
#[derive(Debug, Clone)]
pub enum SymbolKind<'a> {
/// A user-defined function
Function {
/// Names of parameters in order
parameters: Vec<Cow<'a, str>>,
/// Type hint for the return type (if applicable)
return_type: Option<Cow<'a, str>>,
},
/// A system or math syscall
Syscall {
/// Whether it's a System or Math syscall
syscall_type: SyscallType,
/// Number of expected arguments
argument_count: usize,
},
/// A variable declaration
Variable {
/// Type hint for the variable (if applicable)
type_hint: Option<Cow<'a, str>>,
},
}
impl<'a> SymbolKind<'a> {
/// Converts to LSP SymbolKind for protocol compatibility.
fn to_lsp_symbol_kind(&self) -> lsp_types::SymbolKind {
match self {
SymbolKind::Function { .. } => lsp_types::SymbolKind::FUNCTION,
SymbolKind::Syscall { .. } => lsp_types::SymbolKind::FUNCTION, // Syscalls are function-like
SymbolKind::Variable { .. } => lsp_types::SymbolKind::VARIABLE,
}
}
/// Converts to LSP CompletionItemKind for autocomplete filtering.
fn to_lsp_completion_kind(&self) -> lsp_types::CompletionItemKind {
match self {
SymbolKind::Function { .. } => lsp_types::CompletionItemKind::FUNCTION,
SymbolKind::Syscall { .. } => lsp_types::CompletionItemKind::FUNCTION,
SymbolKind::Variable { .. } => lsp_types::CompletionItemKind::VARIABLE,
}
}
/// Returns a human-readable detail string for display in IDEs.
fn detail_string(&self) -> String {
match self {
SymbolKind::Function {
parameters,
return_type,
} => {
let params = parameters
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(", ");
let ret = return_type
.as_ref()
.map(|t| format!(" -> {}", t))
.unwrap_or_default();
format!("fn({}){}", params, ret)
}
SymbolKind::Syscall {
syscall_type,
argument_count,
} => {
format!(
"{}(... {} args)",
match syscall_type {
SyscallType::System => "syscall",
SyscallType::Math => "math",
},
argument_count
)
}
SymbolKind::Variable { type_hint } => type_hint
.as_ref()
.map(|t| t.to_string())
.unwrap_or_else(|| "var".to_string()),
}
}
}
/// Distinguishes between System and Math syscalls.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyscallType {
System,
Math,
}
/// Metadata collected during compilation, including all referenced symbols.
#[derive(Debug, Default)]
pub struct CompilationMetadata<'a> {
/// All symbols encountered during compilation (functions, syscalls, variables)
pub symbols: Vec<SymbolInfo<'a>>,
}
impl<'a> CompilationMetadata<'a> {
/// Creates a new empty compilation metadata.
pub fn new() -> Self {
Self {
symbols: Vec::new(),
}
}
/// Adds a symbol to the metadata.
pub fn add_symbol(&mut self, symbol: SymbolInfo<'a>) {
self.symbols.push(symbol);
}
/// Adds a function symbol.
pub fn add_function(
&mut self,
name: Cow<'a, str>,
parameters: Vec<Cow<'a, str>>,
span: Option<Span>,
) {
self.add_function_with_doc(name, parameters, span, None);
}
/// Adds a function symbol with optional doc comment.
pub fn add_function_with_doc(
&mut self,
name: Cow<'a, str>,
parameters: Vec<Cow<'a, str>>,
span: Option<Span>,
description: Option<Cow<'a, str>>,
) {
self.add_symbol(SymbolInfo {
name,
kind: SymbolKind::Function {
parameters,
return_type: None,
},
span,
description,
});
}
/// Adds a syscall symbol.
pub fn add_syscall(
&mut self,
name: Cow<'a, str>,
syscall_type: SyscallType,
argument_count: usize,
span: Option<Span>,
) {
self.add_syscall_with_doc(name, syscall_type, argument_count, span, None);
}
/// Adds a syscall symbol with optional doc comment.
pub fn add_syscall_with_doc(
&mut self,
name: Cow<'a, str>,
syscall_type: SyscallType,
argument_count: usize,
span: Option<Span>,
description: Option<Cow<'a, str>>,
) {
self.add_symbol(SymbolInfo {
name,
kind: SymbolKind::Syscall {
syscall_type,
argument_count,
},
span,
description,
});
}
/// Adds a variable symbol.
pub fn add_variable(&mut self, name: Cow<'a, str>, span: Option<Span>) {
self.add_variable_with_doc(name, span, None);
}
/// Adds a variable symbol with optional doc comment.
pub fn add_variable_with_doc(
&mut self,
name: Cow<'a, str>,
span: Option<Span>,
description: Option<Cow<'a, str>>,
) {
self.add_symbol(SymbolInfo {
name,
kind: SymbolKind::Variable { type_hint: None },
span,
description,
});
}
/// Returns all symbols of a specific kind.
pub fn symbols_of_kind(&self, kind: &str) -> Vec<&SymbolInfo<'a>> {
self.symbols
.iter()
.filter(|sym| match (&sym.kind, kind) {
(SymbolKind::Function { .. }, "function") => true,
(SymbolKind::Syscall { .. }, "syscall") => true,
(SymbolKind::Variable { .. }, "variable") => true,
_ => false,
})
.collect()
}
/// Converts all symbols to LSP SymbolInformation for protocol compatibility.
pub fn to_lsp_symbols(&self, uri: lsp_types::Uri) -> Vec<lsp_types::SymbolInformation> {
self.symbols
.iter()
.map(|sym| sym.to_lsp_symbol_information(uri.clone()))
.collect()
}
/// Converts all symbols to LSP CompletionItems for autocomplete.
pub fn to_lsp_completion_items(&self) -> Vec<lsp_types::CompletionItem> {
self.symbols
.iter()
.map(|sym| sym.to_lsp_completion_item())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metadata_creation() {
let metadata = CompilationMetadata::new();
assert!(metadata.symbols.is_empty());
}
#[test]
fn test_add_function_symbol() {
let mut metadata = CompilationMetadata::new();
metadata.add_function("test_func".into(), vec!["x".into(), "y".into()], None);
assert_eq!(metadata.symbols.len(), 1);
assert_eq!(metadata.symbols[0].name, "test_func");
}
#[test]
fn test_add_syscall_symbol() {
let mut metadata = CompilationMetadata::new();
metadata.add_syscall("hash".into(), SyscallType::System, 1, None);
assert_eq!(metadata.symbols.len(), 1);
assert_eq!(metadata.symbols[0].name, "hash");
}
#[test]
fn test_symbols_of_kind() {
let mut metadata = CompilationMetadata::new();
metadata.add_function("func1".into(), vec![], None);
metadata.add_syscall("hash".into(), SyscallType::System, 1, None);
metadata.add_variable("x".into(), None);
let functions = metadata.symbols_of_kind("function");
assert_eq!(functions.len(), 1);
let syscalls = metadata.symbols_of_kind("syscall");
assert_eq!(syscalls.len(), 1);
let variables = metadata.symbols_of_kind("variable");
assert_eq!(variables.len(), 1);
}
#[test]
fn test_lsp_completion_items() {
let mut metadata = CompilationMetadata::new();
metadata.add_function("test_func".into(), vec![], None);
metadata.add_syscall("hash".into(), SyscallType::System, 1, None);
metadata.add_variable("x".into(), None);
let completions = metadata.to_lsp_completion_items();
assert_eq!(completions.len(), 3);
// Verify function
assert_eq!(completions[0].label, "test_func");
assert_eq!(
completions[0].kind,
Some(lsp_types::CompletionItemKind::FUNCTION)
);
// Verify syscall
assert_eq!(completions[1].label, "hash");
assert_eq!(
completions[1].kind,
Some(lsp_types::CompletionItemKind::FUNCTION)
);
// Verify variable
assert_eq!(completions[2].label, "x");
assert_eq!(
completions[2].kind,
Some(lsp_types::CompletionItemKind::VARIABLE)
);
}
}

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,379 @@
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(())
}
#[test]
fn device_index_read() -> anyhow::Result<()> {
let compiled = compile! {
check "
device printer = \"d0\";
let value = printer[255];
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
get r1 d0 255
move r8 r1
"
}
);
Ok(())
}
#[test]
fn device_index_write() -> anyhow::Result<()> {
let compiled = compile! {
check "
device printer = \"d0\";
printer[255] = 42;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
put d0 255 42
"
}
);
Ok(())
}
#[test]
fn device_index_db_not_allowed() -> anyhow::Result<()> {
let compiled = compile! {
check "
device stack = \"db\";
let x = stack[10];
"
};
assert!(
!compiled.errors.is_empty(),
"Expected error for db indexing"
);
assert!(
compiled.errors[0]
.to_string()
.contains("Direct stack access on 'db' is not yet supported"),
"Expected db restriction error"
);
Ok(())
}
#[test]
fn device_index_db_write_not_allowed() -> anyhow::Result<()> {
let compiled = compile! {
check "
device stack = \"db\";
stack[10] = 42;
"
};
assert!(
!compiled.errors.is_empty(),
"Expected error for db indexing"
);
assert!(
compiled.errors[0]
.to_string()
.contains("Direct stack access on 'db' is not yet supported"),
"Expected db restriction error"
);
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,70 @@ 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().errors 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,
}
}};
(metadata $source:expr) => {{
let compiler = crate::Compiler::new(
parser::Parser::new(tokenizer::Tokenizer::from($source)),
None,
);
let res = compiler.compile();
res.metadata
}}; }};
} }
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 symbol_documentation;
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

@@ -0,0 +1,120 @@
#[cfg(test)]
mod test {
use anyhow::Result;
#[test]
fn test_variable_doc_comment() -> Result<()> {
let metadata = compile!(metadata "/// this is a documented variable\nlet myVar = 42;");
let var_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "myVar")
.expect("myVar symbol not found");
assert_eq!(
var_symbol.description.as_ref().map(|d| d.as_ref()),
Some("this is a documented variable")
);
Ok(())
}
#[test]
fn test_const_doc_comment() -> Result<()> {
let metadata = compile!(metadata "/// const documentation\nconst myConst = 100;");
let const_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "myConst")
.expect("myConst symbol not found");
assert_eq!(
const_symbol.description.as_ref().map(|d| d.as_ref()),
Some("const documentation")
);
Ok(())
}
#[test]
fn test_device_doc_comment() -> Result<()> {
let metadata = compile!(metadata "/// device documentation\ndevice myDevice = \"d0\";");
let device_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "myDevice")
.expect("myDevice symbol not found");
assert_eq!(
device_symbol.description.as_ref().map(|d| d.as_ref()),
Some("device documentation")
);
Ok(())
}
#[test]
fn test_function_doc_comment() -> Result<()> {
let metadata = compile!(metadata "/// function documentation\nfn test() { }");
let fn_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "test")
.expect("test symbol not found");
assert_eq!(
fn_symbol.description.as_ref().map(|d| d.as_ref()),
Some("function documentation")
);
Ok(())
}
#[test]
fn test_syscall_documentation() -> Result<()> {
let metadata = compile!(metadata "fn test() { clr(d0); }");
let clr_symbol = metadata
.symbols
.iter()
.find(|s| s.name == "clr")
.expect("clr syscall not found");
// clr should have its built-in documentation
assert!(clr_symbol.description.is_some());
assert!(!clr_symbol.description.as_ref().unwrap().is_empty());
Ok(())
}
#[test]
fn test_variable_references_have_tooltips() -> Result<()> {
let metadata = compile!(metadata "/// documented variable\nlet myVar = 5;\nlet x = myVar + 2;\nmyVar = 10;");
// Count how many times 'myVar' appears in symbols
let myvar_symbols: Vec<_> = metadata
.symbols
.iter()
.filter(|s| s.name == "myVar")
.collect();
// We should have at least 2: declaration + 1 reference (in myVar + 2)
// The assignment `myVar = 10` is a write, not a read, so doesn't create a reference
assert!(
myvar_symbols.len() >= 2,
"Expected at least 2 'myVar' symbols (declaration + reference), got {}",
myvar_symbols.len()
);
// All should have the same description
let expected_desc = "documented variable";
for sym in &myvar_symbols {
assert_eq!(
sym.description.as_ref().map(|d| d.as_ref()),
Some(expected_desc),
"Symbol description mismatch at {:?}",
sym.span
);
}
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,100 @@ 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(())
}
#[test]
fn test_clr() -> anyhow::Result<()> {
let compiled = compile! {
check
"
device stackDevice = \"d0\";
clr(stackDevice);
let deviceRef = 5;
clr(deviceRef);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
clr d0
move r8 5
clr r8
"
}
);
Ok(())
}
#[test]
fn test_rmap() -> anyhow::Result<()> {
let compiled = compile! {
check
"
device printer = \"d0\";
let reagentHash = 12345;
let itemHash = rmap(printer, reagentHash);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 12345
rmap r15 d0 r8
move r9 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

@@ -5,11 +5,14 @@ macro_rules! with_syscalls {
// Big names // Big names
"yield", "yield",
"sleep", "sleep",
"clr",
"hash", "hash",
"load", "load",
"loadBatched", "loadBatched",
"loadBatchedNamed", "loadBatchedNamed",
"loadSlot", "loadSlot",
"loadReagent",
"rmap",
"set", "set",
"setBatched", "setBatched",
"setBatchedNamed", "setBatchedNamed",
@@ -35,6 +38,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,381 @@
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>),
/// `rmap register device reagentHash` - Resolve Reagent to Item Hash
Rmap(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` - Bitwise AND
And(Operand<'a>, Operand<'a>, Operand<'a>),
/// `or dst a b` - Bitwise OR
Or(Operand<'a>, Operand<'a>, Operand<'a>),
/// `xor dst a b` - Bitwise XOR
Xor(Operand<'a>, Operand<'a>, Operand<'a>),
/// `nor dst a b` - Bitwise NOR
Nor(Operand<'a>, Operand<'a>, Operand<'a>),
/// `not dst a` - Bitwise NOT
Not(Operand<'a>, Operand<'a>),
/// `sll dst a b` - Logical Left Shift
Sll(Operand<'a>, Operand<'a>, Operand<'a>),
/// `sra dst a b` - Arithmetic Right Shift
Sra(Operand<'a>, Operand<'a>, Operand<'a>),
/// `srl dst a b` - Logical Right Shift
Srl(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>),
/// `clr val` - Clear stack memory on device
Clr(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::Rmap(reg, device, reagent_hash) => {
write!(f, "rmap {} {} {}", reg, device, 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::Nor(dst, a, b) => write!(f, "nor {} {} {}", dst, a, b),
Instruction::Not(dst, a) => write!(f, "not {} {}", dst, a),
Instruction::Sll(dst, a, b) => write!(f, "sll {} {} {}", dst, a, b),
Instruction::Sra(dst, a, b) => write!(f, "sra {} {} {}", dst, a, b),
Instruction::Srl(dst, a, b) => write!(f, "srl {} {} {}", 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::Clr(val) => write!(f, "clr {}", 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,112 @@
#[cfg(test)]
mod bitwise_tests {
use crate::common::compile_with_and_without_optimization;
use indoc::indoc;
#[test]
fn test_bitwise_operations() {
let source = indoc! {"
let a = 5;
let b = 3;
let and_result = a & b;
let or_result = a | b;
let xor_result = a ^ b;
let not_result = ~a;
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_bitwise_shifts() {
let source = indoc! {"
let x = 8;
let left_shift = x << 2;
let arithmetic_shift = x >> 1;
let logical_shift = x >>> 1;
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_bitwise_constant_folding() {
let source = indoc! {"
let packed = (1 << 16) & (255 << 8) & 2;
let mask = 0xFF & 0x0F;
let combined = (15 | 4096);
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_bitwise_with_variables() {
let source = indoc! {"
fn pack_bits(high, low) {
let packed = (high << 8) | low;
return packed;
}
fn extract_bits(value) {
let high = (value >> 8) & 0xFF;
let low = value & 0xFF;
return (high, low);
}
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_complex_bit_manipulation() {
let source = indoc! {"
fn encode_flags(enabled, mode, priority) {
let flag_byte = (enabled << 7) | (mode << 4) | priority;
return flag_byte;
}
fn decode_flags(byte) {
let enabled = (byte >> 7) & 1;
let mode = (byte >> 4) & 0x7;
let priority = byte & 0xF;
return (enabled, mode, priority);
}
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_sorter_bitwise_operations() {
let source = indoc! {r#"
device self = "db";
device sorter = "d0";
loop {
yield();
// allow Hay with an op_code of `1`
sorter[0] = (hash("ItemCropHay") << 8) | 1;
}
"#};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_bitwise_with_const() {
let source = indoc! {r#"
device sorterOutput = "d0";
const ingotSortClass = 19;
const equals = 0;
loop {
yield();
clr(sorterOutput);
sorterOutput[0] = (ingotSortClass << 16) | (equals << 8) | 3;
}
"#};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
}

View File

@@ -0,0 +1,46 @@
use compiler::Compiler;
use parser::Parser;
use tokenizer::Tokenizer;
/// Compile Slang source code and return both unoptimized and optimized output
pub 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();
// 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
)
}

View File

@@ -0,0 +1,107 @@
#[cfg(test)]
mod device_indexing_tests {
use crate::common::compile_with_and_without_optimization;
use indoc::indoc;
#[test]
fn test_device_indexing_with_hash_and_binary_literals() {
let source = indoc! {"
device printer = \"d0\";
let item_type = hash(\"ItemIronIngot\");
let quality = 16;
let quantity = 7;
// Pack into a single value using bitwise operations
// Format: (itemHash << 16) | (quality << 8) | quantity
let packed = (item_type << 16) | (quality << 8) | quantity;
// Write to device stack at address 255
let addr = 255;
printer[addr] = packed;
// Read it back
let result = printer[addr];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_device_indexing_with_computed_index() {
let source = indoc! {"
device storage = \"d1\";
let base_addr = 10;
let offset = 5;
let index = base_addr + offset;
let value = 42;
storage[index] = value;
let retrieved = storage[index];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_device_indexing_with_binary_literals() {
let source = indoc! {"
device mem = \"d0\";
// Binary literals for bitwise operations
let flags = 0b1010_0101;
let mask = 0b1111_0000;
let masked = flags & mask;
// Write to device
mem[0] = masked;
// Read back
let read_value = mem[0];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_device_indexing_complex_expression() {
let source = indoc! {"
device db = \"d0\";
let item = hash(\"ItemCopper\");
let quality = 5;
let quantity = 100;
// Complex bitwise expression
let packed = (item << 16) | ((quality & 0xFF) << 8) | (quantity & 0xFF);
// Index with computed address
let slot = 1;
let address = slot * 256 + 100;
db[address] = packed;
// Read back with different computation
let read_addr = (slot + 0) * 256 + 100;
let stored_value = db[read_addr];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_device_indexing_optimization_folds_constants() {
let source = indoc! {"
device cache = \"d0\";
// This should optimize to a single constant
let packed_constant = (hash(\"ItemSilver\") << 16) | (10 << 8) | 50;
cache[255] = packed_constant;
let result = cache[255];
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
}

View File

@@ -0,0 +1,48 @@
#[cfg(test)]
mod function_tests {
use crate::common::compile_with_and_without_optimization;
use indoc::indoc;
#[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_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_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);
}
}

View File

@@ -0,0 +1,86 @@
//! 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 bitwise_tests;
#[cfg(test)]
mod common;
#[cfg(test)]
mod device_indexing_tests;
#[cfg(test)]
mod function_tests;
#[cfg(test)]
mod number_literal_tests;
#[cfg(test)]
mod optimization_tests;
#[cfg(test)]
mod integration_tests {
use crate::common::compile_with_and_without_optimization;
use indoc::indoc;
#[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,29 @@
#[cfg(test)]
mod number_literal_tests {
use crate::common::compile_with_and_without_optimization;
use indoc::indoc;
#[test]
fn test_binary_literals() {
let source = indoc! {"
let binary = 0b1010_1100;
let octal = 0o755;
let hex_upper = 0xDEAD_BEEF;
let hex_lower = 0xcafe;
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
#[test]
fn test_number_literal_optimization() {
let source = indoc! {"
let decimal = 42_000;
let negative_hex = -0xFF;
let negative_binary = -0b1111_0000;
let temp_c = 100c;
"};
let output = compile_with_and_without_optimization(source);
insta::assert_snapshot!(output);
}
}

View File

@@ -0,0 +1,82 @@
#[cfg(test)]
mod optimization_tests {
use crate::common::compile_with_and_without_optimization;
use indoc::indoc;
#[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_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);
}
}

View File

@@ -0,0 +1,18 @@
---
source: libs/integration_tests/src/bitwise_tests.rs
assertion_line: 40
expression: output
---
## Unoptimized Output
j main
main:
move r8 0
move r9 15
move r10 4111
## Optimized Output
move r8 0
move r9 15
move r10 4111

View File

@@ -0,0 +1,32 @@
---
source: libs/integration_tests/src/bitwise_tests.rs
assertion_line: 17
expression: output
---
## Unoptimized Output
j main
main:
move r8 5
move r9 3
and r1 r8 r9
move r10 r1
or r2 r8 r9
move r11 r2
xor r3 r8 r9
move r12 r3
not r4 r8
move r13 r4
## Optimized Output
move r8 5
move r9 3
move r1 1
move r10 1
move r2 7
move r11 7
move r3 6
move r12 6
not r4 r8
move r13 r4

View File

@@ -0,0 +1,26 @@
---
source: libs/integration_tests/src/bitwise_tests.rs
assertion_line: 29
expression: output
---
## Unoptimized Output
j main
main:
move r8 8
sll r1 r8 2
move r9 r1
sra r2 r8 1
move r10 r2
srl r3 r8 1
move r11 r3
## Optimized Output
move r8 8
move r1 32
move r9 32
move r2 4
move r10 4
move r3 4
move r11 4

View File

@@ -0,0 +1,21 @@
---
source: libs/integration_tests/src/bitwise_tests.rs
expression: output
---
## Unoptimized Output
j main
main:
__internal_L1:
yield
clr d0
put d0 0 1245187
j __internal_L1
__internal_L2:
## Optimized Output
yield
clr d0
put d0 0 1245187
j 0

View File

@@ -0,0 +1,67 @@
---
source: libs/integration_tests/src/bitwise_tests.rs
assertion_line: 57
expression: output
---
## Unoptimized Output
j main
pack_bits:
pop r8
pop r9
push sp
push ra
sll r1 r9 8
or r2 r1 r8
move r10 r2
move r15 r10
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
extract_bits:
pop r8
push sp
push ra
sra r1 r8 8
and r2 r1 255
move r9 r2
and r3 r8 255
move r10 r3
push r9
push r10
sub r0 sp 4
get r0 db r0
move r15 r0
j __internal_L2
__internal_L2:
sub r0 sp 3
get ra db r0
j ra
## Optimized Output
j main
pop r8
pop r9
push sp
push ra
sll r1 r9 8
or r15 r1 r8
pop ra
pop sp
j ra
pop r8
push sp
push ra
sra r1 r8 8
and r9 r1 255
and r10 r8 255
push r9
push r10
sub r0 sp 4
get r15 db r0
sub r0 sp 3
get ra db r0
j ra

View File

@@ -0,0 +1,80 @@
---
source: libs/integration_tests/src/bitwise_tests.rs
assertion_line: 75
expression: output
---
## Unoptimized Output
j main
encode_flags:
pop r8
pop r9
pop r10
push sp
push ra
sll r1 r10 7
sll r2 r9 4
or r3 r1 r2
or r4 r3 r8
move r11 r4
move r15 r11
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
decode_flags:
pop r8
push sp
push ra
sra r1 r8 7
and r2 r1 1
move r9 r2
sra r3 r8 4
and r4 r3 7
move r10 r4
and r5 r8 15
move r11 r5
push r9
push r10
push r11
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
## Optimized Output
j main
pop r8
pop r9
pop r10
push sp
push ra
sll r1 r10 7
sll r2 r9 4
or r3 r1 r2
or r15 r3 r8
pop ra
pop sp
j ra
pop r8
push sp
push ra
sra r1 r8 7
and r9 r1 1
sra r3 r8 4
and r10 r3 7
and r11 r8 15
push r9
push r10
push r11
sub r0 sp 5
get r15 db r0
sub r0 sp 4
get ra db r0
j ra

View File

@@ -0,0 +1,20 @@
---
source: libs/integration_tests/src/bitwise_tests.rs
assertion_line: 91
expression: output
---
## Unoptimized Output
j main
main:
__internal_L1:
yield
put d0 0 55164456193
j __internal_L1
__internal_L2:
## Optimized Output
yield
put d0 0 55164456193
j 0

View File

@@ -0,0 +1,54 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 90
expression: output
---
## Unoptimized Output
j main
main:
move r8 r15
move r9 5
move r10 100
sll r1 r8 16
and r2 r9 255
sll r3 r2 8
or r4 r1 r3
and r5 r10 255
or r6 r4 r5
move r11 r6
move r12 1
mul r7 r12 256
add r2 r7 100
move r13 r2
put d0 r13 r11
add r1 r12 0
mul r3 r1 256
add r4 r3 100
move r14 r4
get r5 d0 r14
push r5
sub sp sp 1
## Optimized Output
move r8 r15
move r9 5
move r10 100
sll r1 r8 16
move r3 1280
or r4 r1 r3
move r5 100
or r11 r4 r5
move r12 1
move r7 256
move r2 356
move r13 356
put d0 r13 r11
move r1 1
move r3 256
move r4 356
move r14 356
get r5 d0 r14
push r5
sub sp sp 1

View File

@@ -0,0 +1,19 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 105
expression: output
---
## Unoptimized Output
j main
main:
move r8 -62440604628430
put d0 255 r8
get r1 d0 255
move r9 r1
## Optimized Output
move r8 -62440604628430
put d0 255 r8
get r9 d0 255

View File

@@ -0,0 +1,25 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 65
expression: output
---
## Unoptimized Output
j main
main:
move r8 165
move r9 240
and r1 r8 r9
move r10 r1
put d0 0 r10
get r2 d0 0
move r11 r2
## Optimized Output
move r8 165
move r9 240
move r1 160
move r10 160
put d0 0 r10
get r11 d0 0

View File

@@ -0,0 +1,27 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 45
expression: output
---
## Unoptimized Output
j main
main:
move r8 10
move r9 5
add r1 r8 r9
move r10 r1
move r11 42
put d1 r10 r11
get r2 d1 r10
move r12 r2
## Optimized Output
move r8 10
move r9 5
move r1 15
move r10 15
move r11 42
put d1 r10 r11
get r12 d1 r10

View File

@@ -0,0 +1,34 @@
---
source: libs/integration_tests/src/device_indexing_tests.rs
assertion_line: 27
expression: output
---
## Unoptimized Output
j main
main:
move r8 r15
move r9 16
move r10 7
sll r1 r8 16
sll r2 r9 8
or r3 r1 r2
or r4 r3 r10
move r11 r4
move r12 255
put d0 r12 r11
get r5 d0 r12
move r13 r5
## Optimized Output
move r8 r15
move r9 16
move r10 7
sll r1 r8 16
move r2 4096
or r3 r1 r2
or r11 r3 r10
move r12 255
put d0 r12 r11
get r13 d0 r12

View File

@@ -0,0 +1,52 @@
---
source: libs/integration_tests/src/function_tests.rs
assertion_line: 20
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,30 @@
---
source: libs/integration_tests/src/function_tests.rs
assertion_line: 31
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/function_tests.rs
assertion_line: 46
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,26 @@
---
source: libs/integration_tests/src/function_tests.rs
assertion_line: 10
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,224 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 54
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,112 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 61
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,27 @@
---
source: libs/integration_tests/src/lib.rs
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,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,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,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,93 @@
---
source: libs/integration_tests/src/lib.rs
assertion_line: 47
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,20 @@
---
source: libs/integration_tests/src/number_literal_tests.rs
assertion_line: 15
expression: output
---
## Unoptimized Output
j main
main:
move r8 172
move r9 493
move r10 3735928559
move r11 51966
## Optimized Output
move r8 172
move r9 493
move r10 3735928559
move r11 51966

View File

@@ -0,0 +1,20 @@
---
source: libs/integration_tests/src/number_literal_tests.rs
assertion_line: 27
expression: output
---
## Unoptimized Output
j main
main:
move r8 42000
move r9 -255
move r10 -240
move r11 373.15
## Optimized Output
move r8 42000
move r9 -255
move r10 -240
move r11 373.15

View File

@@ -0,0 +1,18 @@
---
source: libs/integration_tests/src/optimization_tests.rs
assertion_line: 17
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/optimization_tests.rs
assertion_line: 80
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,14 @@
---
source: libs/integration_tests/src/optimization_tests.rs
assertion_line: 10
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/optimization_tests.rs
assertion_line: 36
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,34 @@
---
source: libs/integration_tests/src/optimization_tests.rs
assertion_line: 49
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,36 @@
---
source: libs/integration_tests/src/optimization_tests.rs
assertion_line: 66
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,30 @@
---
source: libs/integration_tests/src/optimization_tests.rs
assertion_line: 24
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,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,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,213 @@
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::And(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| x & y),
Instruction::Or(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| x | y),
Instruction::Xor(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| x ^ y),
Instruction::Sll(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| {
if y >= 64 { 0 } else { x << y as u32 }
}),
Instruction::Sra(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| {
if y >= 64 {
if x < 0 { -1 } else { 0 }
} else {
x >> y as u32
}
}),
Instruction::Srl(dst, a, b) => try_fold_bitwise(dst, a, b, &registers, |x, y| {
if y >= 64 {
0
} else {
(x as u64 >> y as u32) as i64
}
}),
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 decimal_to_i64(d: Decimal) -> i64 {
// Convert decimal to i64, truncating if needed
if let Ok(int_val) = d.try_into() {
int_val
} else {
// For very large or very small values, use a default
if d.is_sign_negative() {
i64::MIN
} else {
i64::MAX
}
}
}
fn i64_to_decimal(i: i64) -> Decimal {
Decimal::from(i)
}
fn try_fold_bitwise<'a, F>(
dst: &Operand<'a>,
a: &Operand<'a>,
b: &Operand<'a>,
regs: &[Option<Decimal>; 16],
op: F,
) -> Option<Instruction<'a>>
where
F: Fn(i64, i64) -> i64,
{
let val_a = resolve_value(a, regs)?;
let val_b = resolve_value(b, regs)?;
let result = op(decimal_to_i64(val_a), decimal_to_i64(val_b));
Some(Instruction::Move(
dst.clone(),
Operand::Number(i64_to_decimal(result)),
))
}
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);
}
}

Some files were not shown because too many files have changed in this diff Show More