22 Commits

Author SHA1 Message Date
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
28 changed files with 5125 additions and 274 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,16 @@
# Changelog # 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] [0.4.6]
- Fixed bug in compiler where you were unable to assign a `const` value to - Fixed bug in compiler where you were unable to assign a `const` value to

View File

@@ -2,7 +2,7 @@
<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.4.6</Version> <Version>0.5.0</Version>
<Description> <Description>
[h1]Slang: High-Level Programming for Stationeers[/h1] [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 PluginGuid = "com.biddydev.slang";
public const string PluginName = "Slang"; public const string PluginName = "Slang";
public const string PluginVersion = "0.4.6"; public const string PluginVersion = "0.5.0";
private static Harmony? _harmony; private static Harmony? _harmony;

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.4.2</Version> <Version>0.5.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>

View File

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

View File

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

View File

@@ -4,15 +4,21 @@ 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
@@ -27,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;
@@ -38,8 +44,14 @@ 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
@@ -72,15 +84,21 @@ 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
@@ -95,16 +113,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
@@ -123,15 +147,21 @@ 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
@@ -148,16 +178,22 @@ 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
@@ -175,15 +211,21 @@ fn test_ternary_expression_assignment() -> Result<()> {
#[test] #[test]
fn test_negative_literals() -> Result<()> { fn test_negative_literals() -> Result<()> {
let compiled = compile!( let result = compile!(
debug check
r#" r#"
let item = -10c - 20c; let item = -10c - 20c;
"# "#
); );
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
@@ -198,16 +240,22 @@ fn test_negative_literals() -> Result<()> {
#[test] #[test]
fn test_mismatched_temperature_literals() -> Result<()> { fn test_mismatched_temperature_literals() -> Result<()> {
let compiled = compile!( let result = compile!(
debug check
r#" r#"
let item = -10c - 100k; let item = -10c - 100k;
let item2 = item + 500c; let item2 = item + 500c;
"# "#
); );
assert!(
result.errors.is_empty(),
"Expected no errors, got: {:?}",
result.errors
);
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main

View File

@@ -3,8 +3,8 @@ 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) {
@@ -13,8 +13,14 @@ 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
@@ -33,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) {
@@ -45,8 +51,14 @@ 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
@@ -68,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) {
@@ -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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
@@ -111,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;
@@ -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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main

View File

@@ -3,14 +3,20 @@ 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
@@ -25,15 +31,15 @@ fn no_arguments() -> anyhow::Result<()> {
" "
}; };
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;
@@ -46,8 +52,14 @@ 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
@@ -99,8 +111,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;
@@ -110,8 +122,14 @@ 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
@@ -141,8 +159,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);
@@ -150,8 +168,14 @@ 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
@@ -179,8 +203,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;
@@ -190,8 +214,14 @@ 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
@@ -216,8 +246,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;
@@ -226,8 +256,14 @@ 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

View File

@@ -4,13 +4,19 @@ 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
@@ -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,8 +46,14 @@ 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
@@ -67,14 +79,20 @@ 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
@@ -90,15 +108,21 @@ 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
@@ -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,8 +149,14 @@ 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
@@ -149,15 +179,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
@@ -172,7 +208,7 @@ fn test_const_hash_expr() -> anyhow::Result<()> {
#[test] #[test]
fn test_declaration_is_const() -> anyhow::Result<()> { fn test_declaration_is_const() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
const MAX = 100; const MAX = 100;
@@ -180,8 +216,14 @@ fn test_declaration_is_const() -> 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

View File

@@ -0,0 +1,272 @@
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 ra
l r1 d0 On
move r15 r1
j __internal_L1
__internal_L1:
pop ra
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,611 @@
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 ra
move r8 5
__internal_L1:
pop ra
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 ra
sub r0 sp 2
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
sub sp sp 1
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(())
}

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] #[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; 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); 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:
@@ -67,7 +73,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) {
@@ -79,8 +85,14 @@ 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
@@ -107,14 +119,20 @@ 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:

View File

@@ -3,8 +3,8 @@ 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;
@@ -15,8 +15,14 @@ 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
@@ -42,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;
@@ -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!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
@@ -72,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
@@ -98,15 +116,21 @@ 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
@@ -122,15 +146,21 @@ 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
@@ -146,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;
@@ -156,8 +186,14 @@ 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

View File

@@ -3,8 +3,8 @@ 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 {
@@ -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) // __internal_Labels: L1 (start), L2 (end)
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
@@ -35,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 {
@@ -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) // __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
@@ -74,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) {
@@ -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) // __internal_Labels: L1 (start), L2 (end)
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main
@@ -108,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 {
@@ -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) // __internal_Labels: L1 (start), L2 (end), L3 (if end)
assert_eq!( assert_eq!(
compiled, result.output,
indoc! { indoc! {
" "
j main j main

View File

@@ -5,14 +5,20 @@ 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
@@ -29,14 +35,20 @@ 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
@@ -53,14 +65,20 @@ 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
@@ -77,14 +95,20 @@ 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
@@ -101,14 +125,20 @@ 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
@@ -125,14 +155,20 @@ 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
@@ -149,14 +185,20 @@ 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
@@ -173,14 +215,20 @@ 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
@@ -197,14 +245,20 @@ 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
@@ -221,14 +275,20 @@ 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
@@ -245,15 +305,21 @@ 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
@@ -271,14 +337,20 @@ 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
@@ -295,14 +367,20 @@ 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
@@ -319,14 +397,20 @@ 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
@@ -343,14 +427,20 @@ 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
@@ -367,14 +457,20 @@ 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
@@ -391,14 +487,20 @@ 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

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)] #[cfg_attr(test, macro_export)]
macro_rules! compile { macro_rules! compile {
($source:expr) => {{ ($source:expr) => {{
@@ -27,7 +33,7 @@ macro_rules! compile {
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)),
@@ -35,15 +41,25 @@ macro_rules! compile {
); );
let res = compiler.compile(); let res = compiler.compile();
res.instructions.write(&mut writer)?; res.instructions.write(&mut writer)?;
output!(writer) let output = output!(writer);
crate::test::CompilationCheckResult {
errors: res.errors,
output,
}
}}; }};
} }
mod binary_expression; mod binary_expression;
mod branching; mod branching;
mod declaration_function_invocation; mod declaration_function_invocation;
mod declaration_literal; mod declaration_literal;
mod device_access;
mod edge_cases;
mod error_handling;
mod function_declaration; mod function_declaration;
mod logic_expression; mod logic_expression;
mod loops; mod loops;
mod math_syscall; mod math_syscall;
mod negation_priority;
mod scoping;
mod syscall; mod syscall;
mod tuple_literals;

View File

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

View File

@@ -0,0 +1,452 @@
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 ra
mul r1 r8 2
move r15 r1
j __internal_L1
__internal_L1:
pop ra
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 ra
move r8 10
move r15 r8
j __internal_L1
__internal_L1:
pop ra
j ra
func2:
push ra
move r8 20
move r15 r8
j __internal_L2
__internal_L2:
pop ra
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:
move r15 sp
push ra
push 1
push 2
move r15 1
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] #[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
@@ -27,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;
@@ -36,8 +42,14 @@ 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
@@ -57,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;
@@ -66,8 +78,14 @@ 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
@@ -85,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
@@ -108,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");
@@ -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!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main j main
@@ -134,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";
@@ -142,8 +172,14 @@ 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
@@ -160,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";
@@ -168,8 +204,14 @@ 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
@@ -186,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";
@@ -194,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
@@ -211,7 +259,7 @@ fn test_set_slot() -> anyhow::Result<()> {
#[test] #[test]
fn test_load_reagent() -> anyhow::Result<()> { fn test_load_reagent() -> anyhow::Result<()> {
let compiled = compile! { let compiled = compile! {
debug check
r#" r#"
device thingy = "d0"; 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!( assert_eq!(
compiled, compiled.output,
indoc! { indoc! {
" "
j main 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) => { TokenType::Keyword(Keyword::Device) => {
let spanned_dev = self.spanned(|p| p.device())?; let spanned_dev = self.spanned(|p| p.device())?;
@@ -561,9 +567,7 @@ impl<'a> Parser<'a> {
}) })
} }
TokenType::Symbol(Symbol::LParen) => { TokenType::Symbol(Symbol::LParen) => self.parenthesized_or_tuple()?,
self.spanned(|p| p.priority())?.node.map(|node| *node)
}
TokenType::Symbol(Symbol::Minus) => { TokenType::Symbol(Symbol::Minus) => {
let start_span = self.current_span(); let start_span = self.current_span();
@@ -642,8 +646,8 @@ impl<'a> Parser<'a> {
} }
} }
TokenType::Symbol(Symbol::LParen) => *self TokenType::Symbol(Symbol::LParen) => *self
.spanned(|p| p.priority())? .parenthesized_or_tuple()?
.node .map(Box::new)
.ok_or(Error::UnexpectedEOF)?, .ok_or(Error::UnexpectedEOF)?,
TokenType::Identifier(ref id) if SysCall::is_syscall(id) => { TokenType::Identifier(ref id) if SysCall::is_syscall(id) => {
@@ -774,7 +778,8 @@ impl<'a> Parser<'a> {
| Expression::Ternary(_) | Expression::Ternary(_)
| Expression::Negation(_) | Expression::Negation(_)
| Expression::MemberAccess(_) | Expression::MemberAccess(_)
| Expression::MethodCall(_) => {} | Expression::MethodCall(_)
| Expression::Tuple(_) => {}
_ => { _ => {
return Err(Error::InvalidSyntax( return Err(Error::InvalidSyntax(
self.current_span(), self.current_span(),
@@ -1081,19 +1086,39 @@ impl<'a> Parser<'a> {
end_col: right.span.end_col, end_col: right.span.end_col,
}; };
expressions.insert( // Check if the left side is a tuple, and if so, create a TupleAssignment
i, let node = if let Expression::Tuple(tuple_expr) = &left.node {
Spanned { // Extract variable names from the tuple, handling underscores
let mut names = Vec::new();
for item in &tuple_expr.node {
if let Expression::Variable(var) = &item.node {
names.push(var.clone());
} else {
return Err(Error::InvalidSyntax(
item.span,
String::from("Tuple assignment can only contain variable names"),
));
}
}
Expression::TupleAssignment(Spanned {
span, span,
node: Expression::Assignment(Spanned { node: TupleAssignmentExpression {
names,
value: boxed!(right),
},
})
} else {
Expression::Assignment(Spanned {
span, span,
node: AssignmentExpression { node: AssignmentExpression {
assignee: boxed!(left), assignee: boxed!(left),
expression: boxed!(right), expression: boxed!(right),
}, },
}), })
}, };
);
expressions.insert(i, Spanned { span, node });
} }
} }
operators.retain(|symbol| !matches!(symbol, Symbol::Assign)); operators.retain(|symbol| !matches!(symbol, Symbol::Assign));
@@ -1117,8 +1142,12 @@ impl<'a> Parser<'a> {
expressions.pop().ok_or(Error::UnexpectedEOF) expressions.pop().ok_or(Error::UnexpectedEOF)
} }
fn priority(&mut self) -> Result<Option<Box<Spanned<Expression<'a>>>>, Error<'a>> { fn parenthesized_or_tuple(
&mut self,
) -> Result<Option<Spanned<tree_node::Expression<'a>>>, Error<'a>> {
let start_span = self.current_span();
let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?; let current_token = self.current_token.as_ref().ok_or(Error::UnexpectedEOF)?;
if !token_matches!(current_token, TokenType::Symbol(Symbol::LParen)) { if !token_matches!(current_token, TokenType::Symbol(Symbol::LParen)) {
return Err(Error::UnexpectedToken( return Err(Error::UnexpectedToken(
self.current_span(), self.current_span(),
@@ -1127,17 +1156,112 @@ impl<'a> Parser<'a> {
} }
self.assign_next()?; self.assign_next()?;
let expression = self.expression()?.ok_or(Error::UnexpectedEOF)?;
let current_token = self.get_next()?.ok_or(Error::UnexpectedEOF)?; // Handle empty tuple '()'
if !token_matches!(current_token, TokenType::Symbol(Symbol::RParen)) { if self_matches_peek!(self, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken( self.assign_next()?;
Self::token_to_span(&current_token), let end_span = self.current_span();
current_token, let span = Span {
)); start_line: start_span.start_line,
start_col: start_span.start_col,
end_line: end_span.end_line,
end_col: end_span.end_col,
};
return Ok(Some(Spanned {
span,
node: Expression::Tuple(Spanned { span, node: vec![] }),
}));
} }
Ok(Some(boxed!(expression))) let first_expression = self.expression()?.ok_or(Error::UnexpectedEOF)?;
if self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) {
// It is a tuple
let mut items = vec![first_expression];
while self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) {
// Next toekn is a comma, we need to consume it and advance 1 more time.
self.assign_next()?;
self.assign_next()?;
items.push(self.expression()?.ok_or(Error::UnexpectedEOF)?);
}
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&next), next));
}
let end_span = Self::token_to_span(&next);
let span = Span {
start_line: start_span.start_line,
start_col: start_span.start_col,
end_line: end_span.end_line,
end_col: end_span.end_col,
};
Ok(Some(Spanned {
span,
node: Expression::Tuple(Spanned { span, node: items }),
}))
} else {
// It is just priority
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::RParen)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&next), next));
}
Ok(Some(Spanned {
span: first_expression.span,
node: Expression::Priority(boxed!(first_expression)),
}))
}
}
fn tuple_declaration(&mut self) -> Result<Expression<'a>, Error<'a>> {
// 'let' is consumed before this call
// expect '('
let next = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(next, TokenType::Symbol(Symbol::LParen)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&next), next));
}
let mut names = Vec::new();
while !self_matches_peek!(self, TokenType::Symbol(Symbol::RParen)) {
let token = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
let span = Self::token_to_span(&token);
if let TokenType::Identifier(id) = token.token_type {
names.push(Spanned { span, node: id });
} else {
return Err(Error::UnexpectedToken(span, token));
}
if self_matches_peek!(self, TokenType::Symbol(Symbol::Comma)) {
self.assign_next()?;
}
}
self.assign_next()?; // consume ')'
let assign = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(assign, TokenType::Symbol(Symbol::Assign)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&assign), assign));
}
self.assign_next()?; // Consume the `=`
let value = self.expression()?.ok_or(Error::UnexpectedEOF)?;
let semi = self.get_next()?.ok_or(Error::UnexpectedEOF)?;
if !token_matches!(semi, TokenType::Symbol(Symbol::Semicolon)) {
return Err(Error::UnexpectedToken(Self::token_to_span(&semi), semi));
}
Ok(Expression::TupleDeclaration(Spanned {
span: names.first().map(|n| n.span).unwrap_or(value.span),
node: TupleDeclarationExpression {
names,
value: boxed!(value),
},
}))
} }
fn invocation(&mut self) -> Result<InvocationExpression<'a>, Error<'a>> { fn invocation(&mut self) -> Result<InvocationExpression<'a>, Error<'a>> {

View File

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

View File

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

View File

@@ -115,7 +115,7 @@ macro_rules! keyword {
} }
#[derive(Debug, PartialEq, Hash, Eq, Clone, Logos)] #[derive(Debug, PartialEq, Hash, Eq, Clone, Logos)]
#[logos(skip r"[ \t\f]+")] #[logos(skip r"[ \r\t\f]+")]
#[logos(extras = Extras)] #[logos(extras = Extras)]
#[logos(error(LexError, LexError::from_lexer))] #[logos(error(LexError, LexError::from_lexer))]
pub enum TokenType<'a> { pub enum TokenType<'a> {
@@ -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(())
}
}