27 Commits

Author SHA1 Message Date
f87fdc1b0a Added another test to ensure all 3 tuple scenerios are covered 2025-12-30 20:09:45 -07:00
72e6981176 Update tests to reflect new changes with stack cleanup in functions that return tuples 2025-12-30 20:05:10 -07:00
d83341d90b Update tuples to support member access and function calls 2025-12-30 16:33:11 -07:00
d297f1bd46 Update changelog 2025-12-30 12:52:51 -07:00
90a2becbbb Bump version to 5.0 2025-12-30 12:51:24 -07:00
a53ea7fd13 removed debug variant macro 2025-12-30 12:49:28 -07:00
c1a8af6aa7 Refactored remaining tests to use check macro 2025-12-30 12:34:47 -07:00
8c8ae23a27 wip -- convert remaining tests to use check 2025-12-30 12:28:53 -07:00
04c205b31d Fixed compiler bug as a result of the 'check' test variant 2025-12-30 12:05:54 -07:00
c133dc3c80 Refactor tests to use new check variant 2025-12-30 11:58:31 -07:00
9d8a867e5f Add new macro variant 'check' to ensure there are no errors AND the compiled output matches 2025-12-30 11:53:02 -07:00
e2a45f0d05 Added more tests and updated existing to use snapshot style testing 2025-12-30 11:49:42 -07:00
fc13c465c0 Extract logic into reusable functions for better DRY 2025-12-30 11:21:44 -07:00
1ce3162fc0 Refactor Compiler struct to hold FunctionMetadata struct instead of flattening all that information directly onto the Compiler 2025-12-30 11:15:49 -07:00
3092e97d41 Minor DRY refactor. Added more tuple tests 2025-12-30 02:47:39 -07:00
8029fa82b0 complex tuple expressions supported 2025-12-30 02:38:32 -07:00
6d8a22459c wip 2025-12-30 02:31:21 -07:00
20f0f4b9a1 working tuple types 2025-12-30 00:58:02 -07:00
5a88befac9 tuple return types just about implemented 2025-12-30 00:32:55 -07:00
e94fc0f5de Functions returning tuples somewhat working, but they clobber the popped ra 2025-12-29 23:55:00 -07:00
b51800eb77 wip -- tuples compiling. need more work on function invocations 2025-12-29 23:17:18 -07:00
87951ab12f Support tuple assignment expressions and tuple assignments and declarations with function invocations 2025-12-29 22:33:16 -07:00
00b0d4df26 Create new tuple expression types 2025-12-29 22:17:19 -07:00
6ca53e8959 Merge pull request 'Added support for CRLF windows line endings' (#11) from 41-support-windows-crlf-line-endings into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 33s
CI/CD Pipeline / build (push) Successful in 1m42s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #11
2025-12-29 12:32:59 -07:00
8dfdad3f34 Added support for CRLF windows line endings
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 34s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-29 12:29:01 -07:00
e272737ea2 Merge pull request 'Fixed const -> let bug' (#10) from declaration_const_as_let into master
All checks were successful
CI/CD Pipeline / test (push) Successful in 32s
CI/CD Pipeline / build (push) Successful in 1m43s
CI/CD Pipeline / release (push) Successful in 5s
Reviewed-on: #10
2025-12-29 02:44:50 -07:00
f679601818 Fixed const -> let bug
All checks were successful
CI/CD Pipeline / test (pull_request) Successful in 33s
CI/CD Pipeline / build (pull_request) Has been skipped
CI/CD Pipeline / release (pull_request) Has been skipped
2025-12-29 02:31:23 -07:00
28 changed files with 5351 additions and 262 deletions

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

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

View File

@@ -1,5 +1,21 @@
# Changelog
[0.5.0]
- Added support for tuple types
- Added support for tuple returns from functions
- Added support for ignoring tuple values
- Fixed various compiler bugs
[0.4.7]
- Added support for Windows CRLF endings
[0.4.6]
- Fixed bug in compiler where you were unable to assign a `const` value to
a `let` variable
[0.4.5]
- Fixed issue where after clicking "Cancel" on the IC10 Editor, the side-by-side

View File

@@ -2,7 +2,7 @@
<ModMetadata xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Slang</Name>
<Author>JoeDiertay</Author>
<Version>0.4.5</Version>
<Version>0.5.0</Version>
<Description>
[h1]Slang: High-Level Programming for Stationeers[/h1]

View File

@@ -39,7 +39,7 @@ namespace Slang
{
public const string PluginGuid = "com.biddydev.slang";
public const string PluginName = "Slang";
public const string PluginVersion = "0.4.5";
public const string PluginVersion = "0.5.0";
private static Harmony? _harmony;

View File

@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<AssemblyName>StationeersSlang</AssemblyName>
<Description>Slang Compiler Bridge</Description>
<Version>0.4.2</Version>
<Version>0.5.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
</PropertyGroup>

View File

@@ -930,7 +930,7 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "slang"
version = "0.4.5"
version = "0.5.0"
dependencies = [
"anyhow",
"clap",

View File

@@ -1,6 +1,6 @@
[package]
name = "slang"
version = "0.4.5"
version = "0.5.0"
edition = "2021"
[workspace]

View File

@@ -4,15 +4,21 @@ use pretty_assertions::assert_eq;
#[test]
fn simple_binary_expression() -> Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let i = 1 + 2;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -27,8 +33,8 @@ fn simple_binary_expression() -> Result<()> {
#[test]
fn nested_binary_expressions() -> Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn calculateArgs(arg1, arg2, arg3) {
return (arg1 + arg2) * arg3;
@@ -38,8 +44,14 @@ fn nested_binary_expressions() -> Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -47,6 +59,7 @@ fn nested_binary_expressions() -> Result<()> {
pop r8
pop r9
pop r10
push sp
push ra
add r1 r10 r9
mul r2 r1 r8
@@ -54,6 +67,7 @@ fn nested_binary_expressions() -> Result<()> {
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
push 10
@@ -72,15 +86,21 @@ fn nested_binary_expressions() -> Result<()> {
#[test]
fn stress_test_constant_folding() -> Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let negationHell = (-1 + -2) * (-3 + (-4 * (-5 + -6)));
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -95,16 +115,22 @@ fn stress_test_constant_folding() -> Result<()> {
#[test]
fn test_constant_folding_with_variables_mixed_in() -> Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
r#"
device self = "db";
let i = 1 - 3 * (1 + 123.4) * self.Setting + 245c;
"#
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -123,15 +149,21 @@ fn test_constant_folding_with_variables_mixed_in() -> Result<()> {
#[test]
fn test_ternary_expression() -> Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
r#"
let i = 1 > 2 ? 15 : 20;
"#
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -148,16 +180,22 @@ fn test_ternary_expression() -> Result<()> {
#[test]
fn test_ternary_expression_assignment() -> Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
r#"
let i = 0;
i = 1 > 2 ? 15 : 20;
"#
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -175,15 +213,21 @@ fn test_ternary_expression_assignment() -> Result<()> {
#[test]
fn test_negative_literals() -> Result<()> {
let compiled = compile!(
debug
let result = compile!(
check
r#"
let item = -10c - 20c;
"#
);
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -198,16 +242,22 @@ fn test_negative_literals() -> Result<()> {
#[test]
fn test_mismatched_temperature_literals() -> Result<()> {
let compiled = compile!(
debug
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!(
compiled,
result.output,
indoc! {
"
j main

View File

@@ -3,8 +3,8 @@ use pretty_assertions::assert_eq;
#[test]
fn test_if_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 10;
if (a > 5) {
@@ -13,8 +13,14 @@ fn test_if_statement() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -33,8 +39,8 @@ fn test_if_statement() -> anyhow::Result<()> {
#[test]
fn test_if_else_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
if (10 > 5) {
@@ -45,8 +51,14 @@ fn test_if_else_statement() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -68,8 +80,8 @@ fn test_if_else_statement() -> anyhow::Result<()> {
#[test]
fn test_if_else_if_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
if (a == 1) {
@@ -82,8 +94,14 @@ fn test_if_else_if_statement() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -111,8 +129,8 @@ fn test_if_else_if_statement() -> anyhow::Result<()> {
#[test]
fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 1;
let b = 2;
@@ -129,8 +147,14 @@ fn test_spilled_variable_update_in_branch() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main

View File

@@ -3,21 +3,29 @@ use pretty_assertions::assert_eq;
#[test]
fn no_arguments() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn doSomething() {};
let i = doSomething();
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
let to_test = indoc! {
"
j main
doSomething:
push sp
push ra
__internal_L1:
pop ra
pop sp
j ra
main:
jal doSomething
@@ -25,15 +33,15 @@ fn no_arguments() -> anyhow::Result<()> {
"
};
assert_eq!(compiled, to_test);
assert_eq!(result.output, to_test);
Ok(())
}
#[test]
fn let_var_args() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn mul2(arg1) {
return arg1 * 2;
@@ -46,19 +54,27 @@ fn let_var_args() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
mul2:
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:
__internal_L2:
@@ -99,8 +115,8 @@ fn incorrect_args_count() -> anyhow::Result<()> {
#[test]
fn inline_literal_args() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn doSomething(arg1, arg2) {
return 5;
@@ -110,19 +126,27 @@ fn inline_literal_args() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
doSomething:
pop r8
pop r9
push sp
push ra
move r15 5
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
move r8 123
@@ -141,8 +165,8 @@ fn inline_literal_args() -> anyhow::Result<()> {
#[test]
fn mixed_args() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let arg1 = 123;
let returnValue = doSomething(arg1, 456);
@@ -150,17 +174,25 @@ fn mixed_args() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
doSomething:
pop r8
pop r9
push sp
push ra
__internal_L1:
pop ra
pop sp
j ra
main:
move r8 123
@@ -179,8 +211,8 @@ fn mixed_args() -> anyhow::Result<()> {
#[test]
fn with_return_statement() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn doSomething(arg1) {
return 456;
@@ -190,18 +222,26 @@ fn with_return_statement() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
doSomething:
pop r8
push sp
push ra
move r15 456
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
push 123
@@ -216,8 +256,8 @@ fn with_return_statement() -> anyhow::Result<()> {
#[test]
fn with_negative_return_literal() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
fn doSomething() {
return -1;
@@ -226,16 +266,24 @@ fn with_negative_return_literal() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
doSomething:
push sp
push ra
move r15 -1
__internal_L1:
pop ra
pop sp
j ra
main:
jal doSomething

View File

@@ -4,13 +4,19 @@ use pretty_assertions::assert_eq;
#[test]
fn variable_declaration_numeric_literal() -> anyhow::Result<()> {
let compiled = crate::compile! {
debug r#"
check r#"
let i = 20c;
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -26,7 +32,7 @@ fn variable_declaration_numeric_literal() -> anyhow::Result<()> {
#[test]
fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
let a = 0;
let b = 1;
@@ -40,8 +46,14 @@ fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()>
let j = 9;
"#};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -67,14 +79,20 @@ fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()>
#[test]
fn variable_declaration_negative() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
let i = -1;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -90,15 +108,21 @@ fn variable_declaration_negative() -> anyhow::Result<()> {
#[test]
fn test_boolean_declaration() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
let t = true;
let f = false;
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -115,7 +139,7 @@ fn test_boolean_declaration() -> anyhow::Result<()> {
#[test]
fn test_boolean_return() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
fn getTrue() {
return true;
@@ -125,17 +149,25 @@ fn test_boolean_return() -> anyhow::Result<()> {
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
getTrue:
push sp
push ra
move r15 1
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
jal getTrue
@@ -149,15 +181,21 @@ fn test_boolean_return() -> anyhow::Result<()> {
#[test]
fn test_const_hash_expr() -> anyhow::Result<()> {
let compiled = compile!(debug r#"
let compiled = compile!(check r#"
const nameHash = hash("AccessCard");
device self = "db";
self.Setting = nameHash;
"#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -168,3 +206,34 @@ fn test_const_hash_expr() -> anyhow::Result<()> {
);
Ok(())
}
#[test]
fn test_declaration_is_const() -> anyhow::Result<()> {
let compiled = compile! {
check
r#"
const MAX = 100;
let max = MAX;
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled.output,
indoc! {
"
j main
main:
move r8 100
"
}
);
Ok(())
}

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ use pretty_assertions::assert_eq;
#[test]
fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
let compiled = compile!(debug r#"
let compiled = compile!(check r#"
// we need more than 4 params to 'spill' into a stack var
fn doSomething(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {
return arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8 + arg9;
@@ -13,8 +13,14 @@ fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
let returned = doSomething(item1, 2, 3, 4, 5, 6, 7, 8, 9);
"#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {"
j main
doSomething:
@@ -25,10 +31,11 @@ fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
pop r12
pop r13
pop r14
push sp
push ra
sub r0 sp 3
sub r0 sp 4
get r1 db r0
sub r0 sp 2
sub r0 sp 3
get r2 db r0
add r3 r1 r2
add r4 r3 r14
@@ -42,7 +49,7 @@ fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
j __internal_L1
__internal_L1:
pop ra
sub sp sp 2
pop sp
j ra
main:
move r8 1
@@ -67,7 +74,7 @@ fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> {
#[test]
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
fn doSomething() {
if (1 == 1) {
@@ -79,12 +86,19 @@ fn test_early_return() -> anyhow::Result<()> {
doSomething();
"#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
doSomething:
push sp
push ra
seq r1 1 1
beqz r1 __internal_L2
@@ -94,6 +108,7 @@ fn test_early_return() -> anyhow::Result<()> {
j __internal_L1
__internal_L1:
pop ra
pop sp
j ra
main:
jal doSomething
@@ -107,22 +122,30 @@ fn test_early_return() -> anyhow::Result<()> {
#[test]
fn test_function_declaration_with_register_params() -> anyhow::Result<()> {
let compiled = compile!(debug r#"
let compiled = compile!(check r#"
// This is a test function declaration with no body
fn doSomething(arg1, arg2) {
};
"#);
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {"
j main
doSomething:
pop r8
pop r9
push sp
push ra
__internal_L1:
pop ra
pop sp
j ra
"}
);

View File

@@ -3,8 +3,8 @@ use pretty_assertions::assert_eq;
#[test]
fn test_comparison_expressions() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let isGreater = 10 > 5;
let isLess = 5 < 10;
@@ -15,8 +15,14 @@ fn test_comparison_expressions() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -42,8 +48,8 @@ fn test_comparison_expressions() -> anyhow::Result<()> {
#[test]
fn test_logical_and_or_not() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let logic1 = 1 && 1;
let logic2 = 1 || 0;
@@ -51,8 +57,14 @@ fn test_logical_and_or_not() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -72,15 +84,21 @@ fn test_logical_and_or_not() -> anyhow::Result<()> {
#[test]
fn test_complex_logic() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let logic = (10 > 5) && (5 < 10);
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -98,15 +116,21 @@ fn test_complex_logic() -> anyhow::Result<()> {
#[test]
fn test_math_with_logic() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let logic = (1 + 2) > 1;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -122,15 +146,21 @@ fn test_math_with_logic() -> anyhow::Result<()> {
#[test]
fn test_boolean_in_logic() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let res = true && false;
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -146,8 +176,8 @@ fn test_boolean_in_logic() -> anyhow::Result<()> {
#[test]
fn test_invert_a_boolean() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let i = true;
let y = !i;
@@ -156,8 +186,14 @@ fn test_invert_a_boolean() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!(
compiled,
result.output,
indoc! {
"
j main

View File

@@ -3,8 +3,8 @@ use pretty_assertions::assert_eq;
#[test]
fn test_infinite_loop() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
loop {
@@ -13,9 +13,15 @@ fn test_infinite_loop() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
// __internal_Labels: L1 (start), L2 (end)
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -35,8 +41,8 @@ fn test_infinite_loop() -> anyhow::Result<()> {
#[test]
fn test_loop_break() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
loop {
@@ -48,9 +54,15 @@ fn test_loop_break() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
// __internal_Labels: L1 (start), L2 (end), L3 (if end - implicit else label)
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -74,8 +86,8 @@ fn test_loop_break() -> anyhow::Result<()> {
#[test]
fn test_while_loop() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
"
let a = 0;
while (a < 10) {
@@ -84,9 +96,15 @@ fn test_while_loop() -> anyhow::Result<()> {
"
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
// __internal_Labels: L1 (start), L2 (end)
assert_eq!(
compiled,
result.output,
indoc! {
"
j main
@@ -108,8 +126,8 @@ fn test_while_loop() -> anyhow::Result<()> {
#[test]
fn test_loop_continue() -> anyhow::Result<()> {
let compiled = compile! {
debug
let result = compile! {
check
r#"
let a = 0;
loop {
@@ -122,9 +140,15 @@ fn test_loop_continue() -> anyhow::Result<()> {
"#
};
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
// __internal_Labels: L1 (start), L2 (end), L3 (if end)
assert_eq!(
compiled,
result.output,
indoc! {
"
j main

View File

@@ -5,14 +5,20 @@ use pretty_assertions::assert_eq;
#[test]
fn test_acos() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = acos(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -29,14 +35,20 @@ fn test_acos() -> Result<()> {
#[test]
fn test_asin() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = asin(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -53,14 +65,20 @@ fn test_asin() -> Result<()> {
#[test]
fn test_atan() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = atan(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -77,14 +95,20 @@ fn test_atan() -> Result<()> {
#[test]
fn test_atan2() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = atan2(123, 456);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -101,14 +125,20 @@ fn test_atan2() -> Result<()> {
#[test]
fn test_abs() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = abs(-123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -125,14 +155,20 @@ fn test_abs() -> Result<()> {
#[test]
fn test_ceil() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = ceil(123.90);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -149,14 +185,20 @@ fn test_ceil() -> Result<()> {
#[test]
fn test_cos() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = cos(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -173,14 +215,20 @@ fn test_cos() -> Result<()> {
#[test]
fn test_floor() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = floor(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -197,14 +245,20 @@ fn test_floor() -> Result<()> {
#[test]
fn test_log() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = log(123);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -221,14 +275,20 @@ fn test_log() -> Result<()> {
#[test]
fn test_max() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = max(123, 456);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -245,15 +305,21 @@ fn test_max() -> Result<()> {
#[test]
fn test_max_from_game() -> Result<()> {
let compiled = compile! {
debug
check
r#"
let item = 0;
item = max(1 + 2, 2);
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -271,14 +337,20 @@ fn test_max_from_game() -> Result<()> {
#[test]
fn test_min() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = min(123, 456);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -295,14 +367,20 @@ fn test_min() -> Result<()> {
#[test]
fn test_rand() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = rand();
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -319,14 +397,20 @@ fn test_rand() -> Result<()> {
#[test]
fn test_sin() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = sin(3);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -343,14 +427,20 @@ fn test_sin() -> Result<()> {
#[test]
fn test_sqrt() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = sqrt(3);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -367,14 +457,20 @@ fn test_sqrt() -> Result<()> {
#[test]
fn test_tan() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = tan(3);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -391,14 +487,20 @@ fn test_tan() -> Result<()> {
#[test]
fn test_trunc() -> Result<()> {
let compiled = compile! {
debug
check
"
let i = trunc(3.234);
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main

View File

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

View File

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

View File

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

View File

@@ -4,14 +4,20 @@ use pretty_assertions::assert_eq;
#[test]
fn test_yield() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
yield();
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -27,7 +33,7 @@ fn test_yield() -> anyhow::Result<()> {
#[test]
fn test_sleep() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
"
sleep(3);
let sleepAmount = 15;
@@ -36,8 +42,14 @@ fn test_sleep() -> anyhow::Result<()> {
"
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -57,7 +69,7 @@ fn test_sleep() -> anyhow::Result<()> {
#[test]
fn test_set_on_device() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device airConditioner = "d0";
let internalTemp = 20c;
@@ -66,8 +78,14 @@ fn test_set_on_device() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -85,15 +103,21 @@ fn test_set_on_device() -> anyhow::Result<()> {
#[test]
fn test_set_on_device_batched() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
const doorHash = hash("Door");
setBatched(doorHash, "Lock", true);
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
r#"
j main
@@ -108,7 +132,7 @@ fn test_set_on_device_batched() -> anyhow::Result<()> {
#[test]
fn test_set_on_device_batched_named() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device dev = "d0";
const devName = hash("test");
@@ -117,8 +141,14 @@ fn test_set_on_device_batched_named() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -134,7 +164,7 @@ fn test_set_on_device_batched_named() -> anyhow::Result<()> {
#[test]
fn test_load_from_device() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device airCon = "d0";
@@ -142,8 +172,14 @@ fn test_load_from_device() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -160,7 +196,7 @@ fn test_load_from_device() -> anyhow::Result<()> {
#[test]
fn test_load_from_slot() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device airCon = "d0";
@@ -168,8 +204,14 @@ fn test_load_from_slot() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -186,7 +228,7 @@ fn test_load_from_slot() -> anyhow::Result<()> {
#[test]
fn test_set_slot() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device airCon = "d0";
@@ -194,8 +236,14 @@ fn test_set_slot() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main
@@ -211,7 +259,7 @@ fn test_set_slot() -> anyhow::Result<()> {
#[test]
fn test_load_reagent() -> anyhow::Result<()> {
let compiled = compile! {
debug
check
r#"
device thingy = "d0";
@@ -219,8 +267,14 @@ fn test_load_reagent() -> anyhow::Result<()> {
"#
};
assert!(
compiled.errors.is_empty(),
"Expected no errors, got: {:?}",
compiled.errors
);
assert_eq!(
compiled,
compiled.output,
indoc! {
"
j main

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -441,7 +441,13 @@ impl<'a> Parser<'a> {
));
}
TokenType::Keyword(Keyword::Let) => Some(self.spanned(|p| p.declaration())?),
TokenType::Keyword(Keyword::Let) => {
if self_matches_peek!(self, TokenType::Symbol(Symbol::LParen)) {
Some(self.spanned(|p| p.tuple_declaration())?)
} else {
Some(self.spanned(|p| p.declaration())?)
}
}
TokenType::Keyword(Keyword::Device) => {
let spanned_dev = self.spanned(|p| p.device())?;
@@ -561,9 +567,7 @@ impl<'a> Parser<'a> {
})
}
TokenType::Symbol(Symbol::LParen) => {
self.spanned(|p| p.priority())?.node.map(|node| *node)
}
TokenType::Symbol(Symbol::LParen) => self.parenthesized_or_tuple()?,
TokenType::Symbol(Symbol::Minus) => {
let start_span = self.current_span();
@@ -642,8 +646,8 @@ impl<'a> Parser<'a> {
}
}
TokenType::Symbol(Symbol::LParen) => *self
.spanned(|p| p.priority())?
.node
.parenthesized_or_tuple()?
.map(Box::new)
.ok_or(Error::UnexpectedEOF)?,
TokenType::Identifier(ref id) if SysCall::is_syscall(id) => {
@@ -774,7 +778,8 @@ impl<'a> Parser<'a> {
| Expression::Ternary(_)
| Expression::Negation(_)
| Expression::MemberAccess(_)
| Expression::MethodCall(_) => {}
| Expression::MethodCall(_)
| Expression::Tuple(_) => {}
_ => {
return Err(Error::InvalidSyntax(
self.current_span(),
@@ -1081,19 +1086,39 @@ impl<'a> Parser<'a> {
end_col: right.span.end_col,
};
expressions.insert(
i,
Spanned {
// Check if the left side is a tuple, and if so, create a TupleAssignment
let node = if let Expression::Tuple(tuple_expr) = &left.node {
// Extract variable names from the tuple, handling underscores
let mut names = Vec::new();
for item in &tuple_expr.node {
if let Expression::Variable(var) = &item.node {
names.push(var.clone());
} else {
return Err(Error::InvalidSyntax(
item.span,
String::from("Tuple assignment can only contain variable names"),
));
}
}
Expression::TupleAssignment(Spanned {
span,
node: Expression::Assignment(Spanned {
span,
node: AssignmentExpression {
assignee: boxed!(left),
expression: boxed!(right),
},
}),
},
);
node: TupleAssignmentExpression {
names,
value: boxed!(right),
},
})
} else {
Expression::Assignment(Spanned {
span,
node: AssignmentExpression {
assignee: boxed!(left),
expression: boxed!(right),
},
})
};
expressions.insert(i, Spanned { span, node });
}
}
operators.retain(|symbol| !matches!(symbol, Symbol::Assign));
@@ -1117,8 +1142,12 @@ impl<'a> Parser<'a> {
expressions.pop().ok_or(Error::UnexpectedEOF)
}
fn priority(&mut self) -> Result<Option<Box<Spanned<Expression<'a>>>>, Error<'a>> {
fn parenthesized_or_tuple(
&mut self,
) -> Result<Option<Spanned<tree_node::Expression<'a>>>, Error<'a>> {
let start_span = self.current_span();
let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?;
if !token_matches!(current_token, TokenType::Symbol(Symbol::LParen)) {
return Err(Error::UnexpectedToken(
self.current_span(),
@@ -1127,17 +1156,112 @@ impl<'a> Parser<'a> {
}
self.assign_next()?;
let expression = self.expression()?.ok_or(Error::UnexpectedEOF)?;
let current_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(current_token, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken(
Self::token_to_span(&current_token),
current_token,
));
// Handle empty tuple '()'
if self_matches_peek!(self, TokenType::Symbol(Symbol::RParen)) {
self.assign_next()?;
let end_span = self.current_span();
let span = Span {
start_line: start_span.start_line,
start_col: start_span.start_col,
end_line: end_span.end_line,
end_col: end_span.end_col,
};
return Ok(Some(Spanned {
span,
node: Expression::Tuple(Spanned { span, node: vec![] }),
}));
}
Ok(Some(boxed!(expression)))
let first_expression = self.expression()?.ok_or(Error::UnexpectedEOF)?;
if self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) {
// It is a tuple
let mut items = vec![first_expression];
while self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) {
// Next toekn is a comma, we need to consume it and advance 1 more time.
self.assign_next()?;
self.assign_next()?;
items.push(self.expression()?.ok_or(Error::UnexpectedEOF)?);
}
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&next), next));
}
let end_span = Self::token_to_span(&next);
let span = Span {
start_line: start_span.start_line,
start_col: start_span.start_col,
end_line: end_span.end_line,
end_col: end_span.end_col,
};
Ok(Some(Spanned {
span,
node: Expression::Tuple(Spanned { span, node: items }),
}))
} else {
// It is just priority
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&next), next));
}
Ok(Some(Spanned {
span: first_expression.span,
node: Expression::Priority(boxed!(first_expression)),
}))
}
}
fn tuple_declaration(&mut self) -> Result<Expression<'a>, Error<'a>> {
// 'let' is consumed before this call
// expect '('
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::LParen)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&next), next));
}
let mut names = Vec::new();
while !self_matches_peek!(self, TokenType::Symbol(Symbol::RParen)) {
let token = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
let span = Self::token_to_span(&token);
if let TokenType::Identifier(id) = token.token_type {
names.push(Spanned { span, node: id });
} else {
return Err(Error::UnexpectedToken(span, token));
}
if self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) {
self.assign_next()?;
}
}
self.assign_next()?; // consume ')'
let assign = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(assign, TokenType::Symbol(Symbol::Assign)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&assign), assign));
}
self.assign_next()?; // Consume the `=`
let value = self.expression()?.ok_or(Error::UnexpectedEOF)?;
let semi = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(semi, TokenType::Symbol(Symbol::Semicolon)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&semi), semi));
}
Ok(Expression::TupleDeclaration(Spanned {
span: names.first().map(|n| n.span).unwrap_or(value.span),
node: TupleDeclarationExpression {
names,
value: boxed!(value),
},
}))
}
fn invocation(&mut self) -> Result<InvocationExpression<'a>, Error<'a>> {

View File

@@ -112,7 +112,7 @@ fn test_function_invocation() -> Result<()> {
#[test]
fn test_priority_expression() -> Result<()> {
let input = r#"
let x = (4);
let x = (4 + 3);
"#;
let tokenizer = Tokenizer::from(input);
@@ -120,7 +120,7 @@ fn test_priority_expression() -> Result<()> {
let expression = parser.parse()?.unwrap();
assert_eq!("(let x = 4)", expression.to_string());
assert_eq!("(let x = ((4 + 3)))", expression.to_string());
Ok(())
}
@@ -137,7 +137,7 @@ fn test_binary_expression() -> Result<()> {
assert_eq!("(((45 * 2) - (15 / 5)) + (5 ** 2))", expr.to_string());
let expr = parser!("(5 - 2) * 10;").parse()?.unwrap();
assert_eq!("((5 - 2) * 10)", expr.to_string());
assert_eq!("(((5 - 2)) * 10)", expr.to_string());
Ok(())
}
@@ -170,7 +170,7 @@ fn test_ternary_expression() -> Result<()> {
fn test_complex_binary_with_ternary() -> Result<()> {
let expr = parser!("let i = (x ? 1 : 3) * 2;").parse()?.unwrap();
assert_eq!("(let i = ((x ? 1 : 3) * 2))", expr.to_string());
assert_eq!("(let i = (((x ? 1 : 3)) * 2))", expr.to_string());
Ok(())
}
@@ -191,3 +191,99 @@ fn test_nested_ternary_right_associativity() -> Result<()> {
assert_eq!("(let i = (a ? b : (c ? d : e)))", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_declaration() -> Result<()> {
let expr = parser!("let (x, _) = (1, 2);").parse()?.unwrap();
assert_eq!("(let (x, _) = (1, 2))", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment() -> Result<()> {
let expr = parser!("(x, y) = (1, 2);").parse()?.unwrap();
assert_eq!("((x, y) = (1, 2))", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment_with_underscore() -> Result<()> {
let expr = parser!("(x, _) = (1, 2);").parse()?.unwrap();
assert_eq!("((x, _) = (1, 2))", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_declaration_with_function_call() -> Result<()> {
let expr = parser!("let (x, y) = doSomething();").parse()?.unwrap();
assert_eq!("(let (x, y) = doSomething())", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_declaration_with_function_call_with_underscore() -> Result<()> {
let expr = parser!("let (x, _) = doSomething();").parse()?.unwrap();
assert_eq!("(let (x, _) = doSomething())", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment_with_function_call() -> Result<()> {
let expr = parser!("(x, y) = doSomething();").parse()?.unwrap();
assert_eq!("((x, y) = doSomething())", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment_with_function_call_with_underscore() -> Result<()> {
let expr = parser!("(x, _) = doSomething();").parse()?.unwrap();
assert_eq!("((x, _) = doSomething())", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_declaration_with_complex_expressions() -> Result<()> {
let expr = parser!("let (x, y) = (1 + 1, doSomething());")
.parse()?
.unwrap();
assert_eq!("(let (x, y) = ((1 + 1), doSomething()))", expr.to_string());
Ok(())
}
#[test]
fn test_tuple_assignment_with_complex_expressions() -> Result<()> {
let expr = parser!("(x, y) = (doSomething(), 123 / someValue.Setting);")
.parse()?
.unwrap();
assert_eq!(
"((x, y) = (doSomething(), (123 / someValue.Setting)))",
expr.to_string()
);
Ok(())
}
#[test]
fn test_tuple_declaration_all_complex_expressions() -> Result<()> {
let expr = parser!("let (x, y) = (a + b, c * d);").parse()?.unwrap();
assert_eq!("(let (x, y) = ((a + b), (c * d)))", expr.to_string());
Ok(())
}

View File

@@ -245,6 +245,42 @@ impl<'a> std::fmt::Display for DeviceDeclarationExpression<'a> {
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct TupleDeclarationExpression<'a> {
pub names: Vec<Spanned<Cow<'a, str>>>,
pub value: Box<Spanned<Expression<'a>>>,
}
impl<'a> std::fmt::Display for TupleDeclarationExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names = self
.names
.iter()
.map(|n| n.node.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "(let ({}) = {})", names, self.value)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct TupleAssignmentExpression<'a> {
pub names: Vec<Spanned<Cow<'a, str>>>,
pub value: Box<Spanned<Expression<'a>>>,
}
impl<'a> std::fmt::Display for TupleAssignmentExpression<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names = self
.names
.iter()
.map(|n| n.node.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "(({}) = {})", names, self.value)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct IfExpression<'a> {
pub condition: Box<Spanned<Expression<'a>>>,
@@ -348,6 +384,9 @@ pub enum Expression<'a> {
Return(Option<Box<Spanned<Expression<'a>>>>),
Syscall(Spanned<SysCall<'a>>),
Ternary(Spanned<TernaryExpression<'a>>),
Tuple(Spanned<Vec<Spanned<Expression<'a>>>>),
TupleAssignment(Spanned<TupleAssignmentExpression<'a>>),
TupleDeclaration(Spanned<TupleDeclarationExpression<'a>>),
Variable(Spanned<Cow<'a, str>>),
While(Spanned<WhileExpression<'a>>),
}
@@ -384,8 +423,20 @@ impl<'a> std::fmt::Display for Expression<'a> {
),
Expression::Syscall(e) => write!(f, "{}", e),
Expression::Ternary(e) => write!(f, "{}", e),
Expression::Tuple(e) => {
let items = e
.node
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "({})", items)
}
Expression::TupleAssignment(e) => write!(f, "{}", e),
Expression::TupleDeclaration(e) => write!(f, "{}", e),
Expression::Variable(id) => write!(f, "{}", id),
Expression::While(e) => write!(f, "{}", e),
}
}
}

View File

@@ -115,7 +115,7 @@ macro_rules! keyword {
}
#[derive(Debug, PartialEq, Hash, Eq, Clone, Logos)]
#[logos(skip r"[ \t\f]+")]
#[logos(skip r"[ \r\t\f]+")]
#[logos(extras = Extras)]
#[logos(error(LexError, LexError::from_lexer))]
pub enum TokenType<'a> {
@@ -843,3 +843,20 @@ documented! {
}
}
#[cfg(test)]
mod tests {
use super::TokenType;
use logos::Logos;
#[test]
fn test_windows_crlf_endings() -> anyhow::Result<()> {
let src = "let i = 0;\r\n";
let lexer = TokenType::lexer(src);
let tokens = lexer.collect::<Vec<_>>();
assert!(!tokens.iter().any(|res| res.is_err()));
Ok(())
}
}