diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4268ed0..6256674 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -11,6 +11,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ submodules: recursive
- name: Install Rust (1.96)
uses: dtolnay/rust-toolchain@stable
@@ -18,6 +20,9 @@ jobs:
toolchain: 1.96.0
components: rustfmt, clippy
+ - name: Install fasm
+ run: sudo apt-get update && sudo apt-get install -y fasm
+
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
with:
diff --git a/.gitignore b/.gitignore
index e47b9b0..73426f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,8 @@ program.
app
*.o
*.so
+*.ssa
+*.asm
main.s
*.il
bin
@@ -14,5 +16,6 @@ zig-cache/
.zig-cache/
# SDK - tracked as separate submodule
-sdk/
+/sdk/
.wrangler/
+/azlang-docs/book/
diff --git a/azlang-docs/src/SUMMARY.md b/azlang-docs/src/SUMMARY.md
index 0b3c00e..5103b9b 100644
--- a/azlang-docs/src/SUMMARY.md
+++ b/azlang-docs/src/SUMMARY.md
@@ -27,5 +27,5 @@
# Architecture & Compiler Pipeline
- [Pipeline: Tokenizer to AST](./internals/frontend.md)
- [Semantic Validation & Symbol Tables](./internals/validator.md)
-- [QBE Intermediate Representation](./internals/qbe.md)
-- [Direct Assembly & Linking](./internals/assembly_linking.md)
+- [fasm Assembly Emission](./internals/fasm.md)
+- [Single-Pass Assembly & Binary Layout](./internals/direct_assembly.md)
diff --git a/azlang-docs/src/getting-started/hello_world.md b/azlang-docs/src/getting-started/hello_world.md
index 851d08f..d3949ed 100644
--- a/azlang-docs/src/getting-started/hello_world.md
+++ b/azlang-docs/src/getting-started/hello_world.md
@@ -14,10 +14,10 @@ enum FD
Stdout
Stderr
-@link("./sdk/build/write.o")
+@link("./sdk/src/write.s")
op write(FD fd, const str val, const int size): void
-@link("./sdk/build/exit.o")
+@link("./sdk/src/exit.s")
op exit(const int val): void
write(Stdout, "Hello World from AzLang!\n", 25)
@@ -25,26 +25,14 @@ exit(0)
```
### Deconstructing the Code:
-1. `enum FD`: Defines standard file descriptors (`Stdin=0`, `Stdout=1`, `Stderr=2`).
-2. `@link(...)`: Links directly to an object file (`write.o` / `exit.o`) compiled from assembly stubs that perform direct Linux syscalls (`syscall` instruction).
+1. `enum FD`: Defines standard file descriptors (`Stdin=0`, `Stdout=1`, `Stderr=2`). Enum variants are inlined as immediates — no lookup table exists at runtime.
+2. `@link(...)`: Points at a fasm assembly source containing a syscall stub that performs a direct Linux `syscall` instruction. The source is included straight into the generated assembly — there is nothing to pre-compile.
3. `op write(...)`: Declares a callable operation function signature and parameter types.
-4. `write(...)` & `exit(...)`: Calls the operations natively without libc overhead.
+4. `write(...)` & `exit(...)`: Calls the operations natively without libc overhead. The string length `25` accounts for the `\n` escape, which is resolved to a single newline byte at emission time.
---
-## 2. Compiling the SDK Assembly Stubs
-
-If you are working inside the repository or using the SDK:
-
-```bash
-# Build the assembly syscall objects
-as sdk/src/write.s -o sdk/build/write.o
-as sdk/src/exit.s -o sdk/build/exit.o
-```
-
----
-
-## 3. Compiling and Running with AzCLI
+## 2. Compiling with AzCLI
Compile `main.az` using `azcli`:
@@ -55,24 +43,22 @@ azcli build main.az
The AzLang pipeline will:
1. Parse `main.az` into an AST.
2. Validate symbols and types.
-3. Emit QBE Intermediate Language (`main.il`).
-4. Invoke `qbe` to produce `main.s`.
-5. Invoke `as` to produce `main.o`.
-6. Invoke `ld.lld` to link `main.o` together with `write.o` and `exit.o` into the standalone binary `app`.
+3. Emit a single assembly file (`main.asm`) containing the program code, its string data, and `include` directives for `write.s` and `exit.s`.
+4. Invoke `fasm`, which assembles and links that file directly into the standalone binary `main` in one pass.
-> The backend tools (`qbe`, `as`, `ld.lld`) are pulled in automatically by the `azlang` package. You never need to install or call them yourself.
+> The only external tool required is **[fasm](https://flatassembler.net/)**. There is no `qbe`, no `as`, no `ld` and no object files.
Run the resulting executable:
```bash
-./app
+./main
# Output:
# Hello World from AzLang!
```
Check the binary size:
```bash
-ls -lh app
-# Output: ~2.6K
+ls -l main
+# 228 bytes
```
-A complete, functional ELF binary in less than 3 kilobytes!
+A complete, functional ELF executable in 228 bytes!
diff --git a/azlang-docs/src/getting-started/installation.md b/azlang-docs/src/getting-started/installation.md
index 75ac7ac..fd3426e 100644
--- a/azlang-docs/src/getting-started/installation.md
+++ b/azlang-docs/src/getting-started/installation.md
@@ -1,6 +1,6 @@
# Installation
-AzLang ships as a single, self-contained compiler. It has no dependencies: no runtime, no external toolchain, nothing else to install.
+AzLang ships as a single compiler binary. There is no runtime to install, no VM, and no LLVM-sized toolchain — the only external tool the compiler invokes is **fasm**.
---
@@ -14,13 +14,19 @@ This generates the `azcli` binary executable.
---
-## Arch Linux / Package Manager
+## Installing fasm
+
+AzLang emits one `.asm` file per build and hands it to [fasm](https://flatassembler.net/), which assembles and links it into a standalone ELF executable in a single pass.
```bash
-sudo pacman -S azcli
+# Arch Linux
+sudo pacman -S fasm
+
+# Debian / Ubuntu
+sudo apt install fasm
```
-That's it.
+For other platforms, download fasm from and put it on your `PATH`.
---
diff --git a/azlang-docs/src/internals/assembly_linking.md b/azlang-docs/src/internals/assembly_linking.md
deleted file mode 100644
index d18b628..0000000
--- a/azlang-docs/src/internals/assembly_linking.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Direct Assembly & Linking
-
-The final stage of the AzLang compiler pipeline turns intermediate code into a finished, standalone bare-metal executable.
-
----
-
-## 1. Pipeline Execution Flow
-
-AzLang produces machine code through a deterministic three-step lowering pipeline:
-
-1. **Intermediate Lowering**: Source files compile directly to QBE Intermediate Representation (`.il`).
-2. **Assembly Emission**: The QBE backend emits GNU assembly (`.s`), which is assembled into an object file (`.o`).
-3. **Static Linking**: The object file is linked together with any `@link` assembly or C objects via `ld.lld` into the final standalone executable (`app`).
-
----
-
-## 2. Linker Invocation & Target Binary
-
-Because AzLang produces code directly targeting the system entry point `_start` and links custom assembly stubs (`write.o`, `exit.o`), no C runtime or dynamic linker flags are required:
-
-```bash
-ld.lld ./sdk/build/write.o ./sdk/build/exit.o main.o -o app
-```
-
-The resulting `app` is a self-contained ELF executable with:
-- **No dynamic interpreter** (e.g. `/lib64/ld-linux-x86-64.so.2` is not required)
-- **Sub-3KB binary size**
-- **Instantaneous microsecond startup**
diff --git a/azlang-docs/src/internals/direct_assembly.md b/azlang-docs/src/internals/direct_assembly.md
new file mode 100644
index 0000000..343b951
--- /dev/null
+++ b/azlang-docs/src/internals/direct_assembly.md
@@ -0,0 +1,36 @@
+# Single-Pass Assembly & Binary Layout
+
+The final stage of the AzLang compiler pipeline turns the emitted assembly into a finished, standalone bare-metal executable — in one invocation, with no linker.
+
+---
+
+## 1. Pipeline Execution Flow
+
+AzLang produces machine code through a deterministic two-step lowering pipeline:
+
+1. **Assembly Emission**: the validated AST is lowered into one `.asm` file — header, `include` directives for every invoked `@link` source, the program's code, and its string data.
+2. **Single-Pass Assembly**: `fasm` assembles that file directly into the final ELF executable. Assembly and linking happen in the same pass, so no intermediate object files ever exist.
+
+```bash
+fasm main.asm main
+```
+
+Because `@link` targets are fasm assembly *sources* (not pre-built `.o` files), there is nothing to pre-compile: the symbols `write`, `exit`, etc. are defined in the same translation unit and resolved statically at assembly time.
+
+---
+
+## 2. Resulting Binary
+
+AzLang emits `format ELF64 executable` with `entry _start` and a single `readable executable` segment (a 120-byte program header). Because the program targets the system entry point `_start` and inlines its syscall stubs, no C runtime or dynamic linker is involved:
+
+- **No dynamic interpreter** (`/lib64/ld-linux-x86-64.so.2` is not required)
+- **No section headers, no relocations** — a minimal static image
+- **Tiny footprint**: the Hello World program measures **228 bytes**; a comprehensive 60-line multi-module program stays around **2 KB**
+- **Instantaneous startup**: execution begins at `_start` with direct syscalls
+
+```text
+$ file main
+main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header
+$ ./main
+Hello World from AzLang!
+```
diff --git a/azlang-docs/src/internals/fasm.md b/azlang-docs/src/internals/fasm.md
new file mode 100644
index 0000000..6632fea
--- /dev/null
+++ b/azlang-docs/src/internals/fasm.md
@@ -0,0 +1,58 @@
+# fasm Assembly Emission
+
+AzLang compiles code by emitting a single, human-readable **fasm** (flat assembler) assembly file, which `fasm` turns directly into a native ELF executable in one pass.
+
+---
+
+## 1. Why fasm?
+
+fasm was chosen because it aligns with AzLang's minimalist engineering doctrine:
+- **One Pass, One Binary**: fasm assembles *and* links in a single step — there is no separate `as`, no `ld`, no object files.
+- **Small Footprint**: the entire fasm distribution is a single binary of well under 1 MB.
+- **Readable Output**: the emitted `.asm` is clean, commented-friendly x86_64 assembly you can inspect directly.
+
+---
+
+## 2. Sample Assembly Emission
+
+Here is the exact assembly generated for the [Hello World](../getting-started/hello_world.md) program (paths abbreviated; the compiler canonicalizes them to absolute paths):
+
+```asm
+format ELF64 executable
+entry _start
+
+segment readable executable
+
+include "./sdk/src/write.s"
+include "./sdk/src/exit.s"
+
+_start:
+ push rbp
+ mov rbp, rsp
+ sub rsp, 8
+ mov rax, 1
+ push rax
+ lea rax, [main_str0]
+ push rax
+ mov rax, 25
+ push rax
+ pop rdx
+ pop rsi
+ pop rdi
+ call write
+ mov rax, 0
+ push rax
+ pop rdi
+ call exit
+ xor edi, edi
+ mov eax, 60
+ syscall
+
+main_str0 db "Hello World from AzLang!",10,0
+```
+
+AzLang generates this directly from the validated AST:
+- Function calls lower to ordinary `call ` instructions following the System V AMD64 ABI (register arguments, 16-byte stack alignment).
+- String literals become `db` data directives (escape sequences such as `\n` are resolved to bytes at emission time).
+- `@link(...)` sources that are actually invoked are pulled in with `include`, so unused SDK stubs never reach the binary.
+- Programs end with a direct `sys_exit` (`rax = 60`) — no C runtime, no `ret` to nowhere.
diff --git a/azlang-docs/src/internals/qbe.md b/azlang-docs/src/internals/qbe.md
deleted file mode 100644
index eeda824..0000000
--- a/azlang-docs/src/internals/qbe.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# QBE Intermediate Representation
-
-AzLang compiles code through **QBE** (Quick Backend), a lean intermediate representation compiler providing high code quality without the massive overhead and gigabyte dependencies of LLVM.
-
----
-
-## 1. Why QBE?
-
-QBE was chosen because it aligns with AzLang's minimalist engineering doctrine:
-- **Fast Execution**: Compiles IR to native assembly in microseconds.
-- **Small Footprint**: The entire QBE binary is less than 300KB.
-- **Readable IR**: QBE intermediate language (`.il`) is human-readable and clean.
-
----
-
-## 2. Sample QBE IR Emission
-
-Here is the exact intermediate representation generated by AzLang for entry code (`main.il` / `ssample.il`):
-
-```qbe
-data $str0 = { b "Hello World", b 0 }
-
-export function w $_start() {
-@start
- call $write(l 1, l $str0, l 11)
- call $exit(w 0)
- ret
-}
-```
-
-AzLang generates this directly from the validated AST, translating function calls to `call $(...)` and string constants into `.data` declarations.
diff --git a/azlang-docs/src/internals/validator.md b/azlang-docs/src/internals/validator.md
index f82c604..1c10f72 100644
--- a/azlang-docs/src/internals/validator.md
+++ b/azlang-docs/src/internals/validator.md
@@ -10,7 +10,7 @@ During semantic validation, the compiler analyzes program declarations to verify
- **Type Safety**: Variable assignments, arithmetic expressions, and function arguments match expected types.
- **Scope Management**: Variables, constants, and functions are validated within their lexical scopes.
-- **Link Registry**: External function declarations (`@link`) are recorded and prepared for the linker phase.
+- **Link Registry**: External function declarations (`@link`) are recorded as `(operation, source path)` pairs and prepared for assembly emission.
- **Unused & Mutation Tracking**: Identifies immutable violations and unused bindings at compile-time.
---
diff --git a/azlang-docs/src/introduction.md b/azlang-docs/src/introduction.md
index 750a944..2cefe28 100644
--- a/azlang-docs/src/introduction.md
+++ b/azlang-docs/src/introduction.md
@@ -1,6 +1,6 @@
# Introduction to AzLang
-> **AzLang** is a minimal, blazingly fast, and readable systems programming language engineered for direct bare-metal execution, sub-3KB binary output, and zero-runtime overhead.
+> **AzLang** is a minimal, blazingly fast, and readable systems programming language engineered for direct bare-metal execution, sub-kilobyte binary output, and zero-runtime overhead.
AzLang blends **Python-like visual clarity**, **Rust-like memory control**, and **TypeScript-like type safety**, stripping away heavy runtime abstractions and external runtime daemons.
@@ -9,10 +9,10 @@ AzLang blends **Python-like visual clarity**, **Rust-like memory control**, and
## Key Characteristics
- **Zero-VM Execution**: No virtual machines, dynamic garbage collectors, or heavy runtime initialization routines (`crt1.o`). Execution lowers directly to native machine code.
-- **Microsecond Compilations**: A lean frontend passes verified AST directly to intermediate representation and native machine instructions.
-- **Lean Binary Footprint**: Produces standalone, self-contained ELF executables often measuring between **2KB and 3KB**.
+- **Microsecond Compilations**: A lean frontend passes verified AST directly to fasm assembly and native machine instructions in a single pass.
+- **Lean Binary Footprint**: Produces standalone, self-contained ELF executables — a hello world program measures **228 bytes**, and a comprehensive multi-module program stays around **2 KB**.
- **Absence of Moving Parts**: Eliminates runtime state machines, dynamic heap leaks, and unwinding tables by design.
-- **Direct ABI & Syscall Interop**: High-performance systems interfaces via native `@link` decorators and direct kernel syscall invocation (`x86_64` register calling conventions).
+- **Direct ABI & Syscall Interop**: High-performance systems interfaces via native `@link` directives and direct kernel syscall invocation (`x86_64` register calling conventions).
---
@@ -31,18 +31,13 @@ AzLang blends **Python-like visual clarity**, **Rust-like memory control**, and
│ [AST Construction & Validation] │
│ │ │
│ ▼ │
-│ [Direct AST Lowering / QBE IR (.il)] │
+│ [Transpiler → fasm Assembly (.asm)] ──► @link syscall stubs included inline │
│ │ │
│ ▼ │
-│ [Native Assembly Generation (.s)] ──► [Assembler: as] ──► Object (.o) │
-│ │ │
-│ [Direct Syscalls] │
-│ │ │
-│ ▼ │
-│ [Linker: ld.lld] │
-│ │ │
-│ ▼ │
-│ [~2.6 KB Bare-Metal Binary] │
+│ [fasm: single-pass assembler & linker] │
+│ │ │
+│ ▼ │
+│ [~228 Byte Bare-Metal ELF] │
└───────────────────────────────────────────────────────────────────────────────────┘
```
@@ -58,10 +53,10 @@ enum FD
Stdout
Stderr
-@link("./sdk/build/write.o")
+@link("./sdk/src/write.s")
op write(FD fd, const str val, const int size): void
-@link("./sdk/build/exit.o")
+@link("./sdk/src/exit.s")
op exit(const int val): void
write(Stdout, "Hello, Bare Metal AzLang!\n", 26)
diff --git a/azlang-docs/src/philosophy/doctrine.md b/azlang-docs/src/philosophy/doctrine.md
index 640726b..976c04b 100644
--- a/azlang-docs/src/philosophy/doctrine.md
+++ b/azlang-docs/src/philosophy/doctrine.md
@@ -8,7 +8,7 @@ AzLang brings this functional minimalist mindset to software engineering:
┌───────────────────────────────────────────────────────────────────────────────────┐
│ AZLANG FIELD DOCTRINE │
│ │
-│ [Source Code] ──► [Direct AST Lowering] ──► [2.6 KB Bare-Metal Binary] │
+│ [Source Code] ──► [fasm Assembly Emission] ──► [228-Byte Bare-Metal Binary] │
│ │
│ • Self-contained toolchain with zero runtime dependencies. │
│ • Executes anywhere, compiles in microseconds, leaves zero operational footprint.│
@@ -28,8 +28,8 @@ Traditional compiler architectures often assume an infinite logistical tail:
AzLang is designed as a self-contained, field-deployable compiler:
- **No background daemons**: Compiles directly in one shot.
-- **Direct AST Lowering**: AST nodes lower into a lean intermediate representation (QBE / direct IR) without requiring gigabytes of compiler caches.
-- **Microsecond compilation**: The compiler executable can be dropped onto an environment, parse code, validate semantics, emit intermediate code, and link a binary in milliseconds.
+- **Direct AST Lowering**: AST nodes lower straight into readable fasm assembly without requiring gigabytes of compiler caches.
+- **Microsecond compilation**: The compiler executable can be dropped onto an environment, parse code, validate semantics, emit assembly, and link a binary in milliseconds.
---
@@ -43,7 +43,7 @@ Rather than performing multi-pass combinatorial saturations that bloat compile t
| Metric | AzLang Engineering Model | Heavy Industrial Toolchains |
|---|---|---|
| **Compilation Latency** | Microseconds to milliseconds | Seconds to minutes |
-| **Output Binary Size** | Typically 2.0 KB – 3.0 KB | 5 MB – 50 MB |
+| **Output Binary Size** | Hello world: 228 bytes; ~2 KB for multi-module programs | 5 MB – 50 MB |
| **Runtime Dependencies** | None (Direct Kernel Syscalls) | libc, libm, libpthread, dynamic runtimes |
| **Hardware Targets** | Bare metal, microcontrollers, embedded, microservices | Cloud servers, heavy multi-core desktops |
diff --git a/azlang-docs/src/sdk/link.md b/azlang-docs/src/sdk/link.md
new file mode 100644
index 0000000..2b1f00b
--- /dev/null
+++ b/azlang-docs/src/sdk/link.md
@@ -0,0 +1,45 @@
+# Direct ABI Linking (`@link`)
+
+One of AzLang's most powerful capabilities is seamless, zero-overhead interoperability with native assembly code — no C runtime, no FFI wrappers, no dynamic loading.
+
+---
+
+## 1. The `@link` Directive
+
+To call native code, point `@link` at a **fasm assembly source** and declare the operation's signature:
+
+```azlang
+@link("")
+op ():
+```
+
+### Example:
+```azlang
+@link("./sdk/src/write.s")
+op write(FD fd, const str val, const int size): void
+
+@link("./sdk/src/exit.s")
+op exit(const int val): void
+```
+
+---
+
+## 2. Compiler Pipeline Integration
+
+When the AzLang compiler encounters an `@link` directive:
+
+1. **Parser**: The AST node `Statement::ExternalFunctionDef` records:
+ - `name`: Target symbol name (e.g. `write`).
+ - `library`: Path to the assembly source (e.g. `./sdk/src/write.s`).
+ - `params` & `return_typ`: Checked against call sites.
+2. **Validator**: Registers `write` as a valid function and adds `("write", "./sdk/src/write.s")` to the compilation's `link_files` registry.
+3. **Transpiler**: When `write` is actually *called*, the compiler canonicalizes the path and emits it into the generated assembly:
+
+```asm
+include "/abs/path/to/sdk/src/write.s"
+```
+
+ Sources of operations that are never invoked are skipped, and duplicate paths are included only once — unused stubs never reach the binary.
+4. **fasm**: The included source and the generated code live in the same translation unit, so `call write` resolves statically at assembly time.
+
+No dynamic library lookups at runtime, no `dlopen` overhead: the code is assembled directly into the binary image in a single `fasm main.asm main` pass.
diff --git a/azlang-docs/src/sdk/standard_library.md b/azlang-docs/src/sdk/standard_library.md
new file mode 100644
index 0000000..bb68d55
--- /dev/null
+++ b/azlang-docs/src/sdk/standard_library.md
@@ -0,0 +1,65 @@
+# Standard SDK Library
+
+The official AzLang SDK resides in the `sdk/` directory (tracked as a git submodule). It acts as the reference standard library implementation demonstrating how systems programming is conducted in pure AzLang.
+
+---
+
+## 1. Structure of `sdk/`
+
+```text
+sdk/
+├── src/
+│ ├── lib.az # Common types, enums, and @link declarations
+│ ├── write.s # Linux sys_write assembly stub (fasm syntax)
+│ ├── exit.s # Linux sys_exit assembly stub (fasm syntax)
+│ └── fopen.s # Linux sys_open assembly stub (fasm syntax)
+└── README.md
+```
+
+There is no build step: the `.s` files are fasm assembly *sources* referenced by `@link` and included directly into the compiler's generated assembly.
+
+---
+
+## 2. Standard Types in `lib.az`
+
+```azlang
+enum FD
+ Stdin
+ Stdout
+ Stderr
+
+enum FLAG
+ ReadOnly
+ Write
+
+enum FMODE
+ NoCreate
+ Create
+
+@link("./sdk/src/exit.s")
+op exit(const int val): void
+
+@link("./sdk/src/write.s")
+op write(FD fd, const str val, const int size): void
+
+@link("./sdk/src/fopen.s")
+op fopen(const str val, FLAG flag, FMODE mode): File
+```
+
+---
+
+## 3. Sample Application
+
+```azlang
+// hello_world.az
+import lib
+
+write(Stdout, "Hello World\n", 12)
+```
+
+Build it with a single command:
+
+```bash
+azcli build hello_world.az
+./hello_world
+```
diff --git a/azlang-docs/src/sdk/syscalls.md b/azlang-docs/src/sdk/syscalls.md
new file mode 100644
index 0000000..ebbe4c2
--- /dev/null
+++ b/azlang-docs/src/sdk/syscalls.md
@@ -0,0 +1,46 @@
+# Kernel Syscalls & Bare-Metal Stubs
+
+In the AzLang architecture, standard I/O does not rely on the heavy GNU C Library (`glibc` or `musl`). Instead, core operations are mapped directly to Linux kernel system calls using lean assembly stubs.
+
+---
+
+## 1. System Call Assembly Implementations
+
+The AzLang SDK provides plain fasm assembly sources for Linux `x86_64` (fasm defaults to Intel syntax). These files are pulled in by `@link` via `include` — they are never assembled on their own.
+
+### `write.s` — Direct `sys_write` (Syscall 1)
+```asm
+write:
+ mov rax, 1 ; sys_write
+ syscall
+ ret
+```
+
+On `x86_64` Linux ABI:
+- `rdi`: File descriptor (`FD fd`)
+- `rsi`: Buffer pointer (`const str val`)
+- `rdx`: Buffer byte count (`const int size`)
+- `rax`: Syscall number (`1` for `sys_write`)
+
+### `exit.s` — Direct `sys_exit` (Syscall 60)
+```asm
+exit:
+ mov rax, 60 ; sys_exit
+ syscall
+```
+
+### `fopen.s` — Direct `sys_open` (Syscall 2)
+```asm
+fopen:
+ mov rax, 2 ; sys_open
+ syscall
+ ret
+```
+
+---
+
+## 2. Advantages of Direct Syscalls
+
+- **Zero Startup Penalty**: No dynamic symbol relocations or runtime linker (`ld-linux.so`) overhead.
+- **Microscopic Footprint**: Omits megabytes of standard library code that standard C programs pull in.
+- **Security & Attack Surface Reduction**: Eliminates vulnerabilities inside complex libc string/format-string/locale handlers.
diff --git a/azlang-docs/src/syntax/enums.md b/azlang-docs/src/syntax/enums.md
index f528926..92476da 100644
--- a/azlang-docs/src/syntax/enums.md
+++ b/azlang-docs/src/syntax/enums.md
@@ -36,13 +36,22 @@ During the semantic validation stage, enum variants are registered directly as s
- `Stdout -> 1`
- `Stderr -> 2`
-During intermediate code generation, any reference to an enum variant is **directly inlined at compile-time as an immediate byte / literal value** directly into the instruction stream or call site:
-
-```qbe
-# Emitted Intermediate Representation
-# Notice that no stack allocation or copy instruction is generated.
-# The variant is inlined directly as an immediate literal argument:
-call $write(l 1, l $str0, l 27)
+During code generation, any reference to an enum variant is **directly inlined at compile-time as an immediate byte / literal value** directly into the instruction stream or call site:
+
+```asm
+; Emitted assembly
+; Notice that no load from memory or lookup table is generated.
+; The variant is inlined directly as an immediate argument:
+ mov rax, 1
+ push rax
+ lea rax, [main_str0]
+ push rax
+ mov rax, 27
+ push rax
+ pop rdx
+ pop rsi
+ pop rdi
+ call write
```
### Key Architectural Benefits:
@@ -62,10 +71,10 @@ enum FD
Stdout
Stderr
-@link("./sdk/build/write.o")
+@link("./sdk/src/write.s")
op write(FD fd, const str val, const int size): void
-@link("./sdk/build/exit.o")
+@link("./sdk/src/exit.s")
op exit(const int val): void
# Stdout is validated as FD type at compile-time,
diff --git a/azlang-docs/src/syntax/functions.md b/azlang-docs/src/syntax/functions.md
index 1c14819..66330d9 100644
--- a/azlang-docs/src/syntax/functions.md
+++ b/azlang-docs/src/syntax/functions.md
@@ -35,17 +35,17 @@ op log_message(const str msg, const int len): void
## 3. External Operations (`@link` + `op`)
-To declare an interface to an external object file, library, or assembly stub, place the `@link` directive above the `op` signature:
+To declare an interface to an external assembly source, place the `@link` directive above the `op` signature:
```azlang
-@link("./sdk/build/write.o")
+@link("./sdk/src/write.s")
op write(FD fd, const str val, const int size): void
-@link("./sdk/build/exit.o")
+@link("./sdk/src/exit.s")
op exit(const int val): void
-@link("./sdk/build/fopen.o")
+@link("./sdk/src/fopen.s")
op fopen(const str val, FLAG flag, FMODE mode): File
```
-When AzLang compiles this file, it registers the external function signature in its symbol table and automatically adds the linked object file to the final linker invocation (`ld.lld`).
+When AzLang compiles this file, it registers the external function signature in its symbol table and, for every operation that is actually called, emits an `include` directive pulling the assembly source into the generated `.asm` — fasm then resolves the symbol statically in the same pass.
diff --git a/azlang-docs/src/syntax/modules.md b/azlang-docs/src/syntax/modules.md
index cb8dfc4..e56d079 100644
--- a/azlang-docs/src/syntax/modules.md
+++ b/azlang-docs/src/syntax/modules.md
@@ -32,10 +32,10 @@ enum FD
Stdout
Stderr
-@link("./sdk/build/exit.o")
+@link("./sdk/src/exit.s")
op exit(const int val): void
-@link("./sdk/build/write.o")
+@link("./sdk/src/write.s")
op write(FD fd, const str val, const int size): void
```
@@ -46,4 +46,4 @@ op write(FD fd, const str val, const int size): void
When an `import` directive is encountered:
1. The parser resolves the target file in the module search path.
2. The imported module AST is parsed and analyzed.
-3. Symbol tables are merged or qualified, ensuring all linked `.o` files and symbols are provided to the linker.
+3. Symbol tables are merged or qualified, ensuring every `@link` source needed by the combined program is included exactly once in the emitted assembly.
diff --git a/azlang-docs/src/syntax/types.md b/azlang-docs/src/syntax/types.md
index 276d5d5..0739062 100644
--- a/azlang-docs/src/syntax/types.md
+++ b/azlang-docs/src/syntax/types.md
@@ -8,11 +8,11 @@ AzLang provides a focused set of primitive and composite types that map directly
| Type | Description | Machine Mapping |
|---|---|---|
-| `int` | Standard signed integer | 32-bit / 64-bit word (`w` / `l` in QBE) |
+| `int` | Standard signed integer | 32-bit / 64-bit machine word |
| `natural` | Unsigned non-negative integer | Unsigned machine word |
-| `bigint` | Large 64-bit integer | 64-bit long (`l`) |
-| `tinyint` | Small 8-bit / 16-bit integer | Byte / Half-word (`b` / `h`) |
-| `float` | Floating-point number | Single / Double IEEE-754 (`s` / `d`) |
+| `bigint` | Large 64-bit integer | 64-bit register |
+| `tinyint` | Small 8-bit / 16-bit integer | Byte / Half-word |
+| `float` | Floating-point number | Single / Double IEEE-754 |
### Example:
```azlang
diff --git a/compiler/cli b/compiler/cli
deleted file mode 100755
index b240450..0000000
Binary files a/compiler/cli and /dev/null differ
diff --git a/compiler/cli.s b/compiler/cli.s
deleted file mode 100644
index c7e5425..0000000
--- a/compiler/cli.s
+++ /dev/null
@@ -1,25 +0,0 @@
-
-.intel_syntax noprefix
-.text
-.global _start
-
-_start:
- mov rdi, [rsp]
-
- mov rsi, [rsp + 8]
-
- cmp rdi, 1
- jle no_args
-
- mov rdx, [rsp + 16]
-
- mov rax,1
- mov rsi, rdx
- mov rdx, 4
- syscall
-
-
-no_args:
- mov rax, 60
- xor rdi, rdi
- syscall
diff --git a/compiler/main.s b/compiler/main.s
deleted file mode 100644
index 33c2b3f..0000000
--- a/compiler/main.s
+++ /dev/null
@@ -1,25 +0,0 @@
-.data
-.balign 8
-c:
- .ascii "Hello World"
- .byte 0
-/* end data */
-
-.text
-.balign 16
-.globl main
-main:
- endbr64
- pushq %rbp
- movq %rsp, %rbp
- movl $11, %edx
- leaq c(%rip), %rsi
- movl $1, %edi
- callq write
- leave
- ret
-.type main, @function
-.size main, .-main
-/* end function main */
-
-.section .note.GNU-stack,"",@progbits
diff --git a/compiler/main.ssa b/compiler/main.ssa
deleted file mode 100644
index 550b208..0000000
--- a/compiler/main.ssa
+++ /dev/null
@@ -1,10 +0,0 @@
-data $c = { b "Hello World", b 0 }
-
- export function w $main() {
- @start
- %a = w copy 11
-%b = w copy 1
-
- call $write(w %b,l $c,w %a)
- ret
- }
diff --git a/compiler/read b/compiler/read
deleted file mode 100755
index d05f97e..0000000
Binary files a/compiler/read and /dev/null differ
diff --git a/compiler/read.s b/compiler/read.s
deleted file mode 100644
index e0ab653..0000000
--- a/compiler/read.s
+++ /dev/null
@@ -1,59 +0,0 @@
-section .data
- filename db "/etc/os-release", 0 ; Null-terminated filename string
-
-section .bss
- buffer resb 512 ; Reserve a 512-byte buffer for the file contents
- fd resq 1 ; Reserve 8 bytes to store the file descriptor
-
-section .text
- global _start
-
-_start:
- ; 1. OPEN THE FILE (sys_open)
- mov rax, 2 ; Syscall ID for sys_open
- mov rdi, filename ; Pointer to the filename string
- mov rsi, 0 ; Flags: O_RDONLY (0)
- mov rdx, 0 ; Mode: Not creating a file, so 0 is fine
- syscall ; Invoke kernel
-
- cmp rax, 0
- jl error_exit
- mov [fd], rax
-
- ; 2. READ FROM THE FILE (sys_read)
- mov rax, 0 ; Syscall ID for sys_read
- mov rdi, [fd] ; File descriptor we saved earlier
- mov rsi, buffer ; Pointer to our memory buffer
- mov rdx, 512 ; Max bytes to read
- syscall ; Invoke kernel
-
- ; Check if read failed
- cmp rax, 0
- jl error_exit
-
- ; Save the actual number of bytes read to r12 for the write syscall
- mov r12, rax
-
- ; 3. WRITE TO STDOUT (sys_write - so we can see the text)
- mov rax, 1 ; Syscall ID for sys_write
- mov rdi, 1 ; File descriptor: STDOUT (1)
- mov rsi, buffer ; Pointer to the text buffer
- mov rdx, r12 ; Exact count of bytes read from the file
- syscall ; Invoke kernel
-
- ; 4. CLOSE THE FILE (sys_close)
- mov rax, 3 ; Syscall ID for sys_close
- mov rdi, [fd] ; File descriptor to close
- syscall ; Invoke kernel
-
-normal_exit:
- ; 5. EXIT PROGRAM SUCCESSFULLY (sys_exit)
- mov rax, 60 ; Syscall ID for sys_exit
- mov rdi, 0 ; Return status 0
- syscall
-
-error_exit:
- ; EXIT PROGRAM WITH ERROR STATUS
- mov rax, 60 ; Syscall ID for sys_exit
- mov rdi, 1 ; Return status 1 (Error)
- syscall
diff --git a/compiler/src/errors.rs b/compiler/src/errors.rs
index 1bada65..df821a0 100644
--- a/compiler/src/errors.rs
+++ b/compiler/src/errors.rs
@@ -6,14 +6,12 @@ use validator::errors::ValidatorError;
#[derive(Debug, PartialEq)]
pub enum BackendError {
- Qbe,
- BinUtils,
+ Fasm,
}
impl Display for BackendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
- BackendError::Qbe => writeln!(f, "QBE doesn't installed"),
- BackendError::BinUtils => writeln!(f, "binutils doesn't installed"),
+ BackendError::Fasm => writeln!(f, "fasm doesn't installed"),
}
}
}
diff --git a/compiler/src/lib.rs b/compiler/src/lib.rs
index f1c996e..1dd64b3 100644
--- a/compiler/src/lib.rs
+++ b/compiler/src/lib.rs
@@ -7,32 +7,76 @@ use validator::Validator;
mod errors;
mod pipline;
-use crate::{
- errors::CompilerError::{self},
- pipline::{compile_to_obj, executer},
-};
+use crate::{errors::CompilerError::self, pipline::assemble};
const STANDARTLIB: &str = "sdk/src/lib.az";
+pub struct Compiled {
+ pub validator: Validator,
+ pub code: Vec,
+ pub data: Vec,
+ pub called: std::collections::HashSet,
+}
+
pub fn compiler(path: &str) -> Result<(), CompilerError> {
let file_name = Path::new(path)
.file_stem()
.and_then(|name| name.to_str())
.expect("Your file name isn't valid. Please write utf-8 standarts");
- let context = compile_obj(path, true, &file_name)?;
- executer(file_name, context.link_files);
+ let compiled = compile_obj(path, true, file_name)?;
+
+ let mut asm_path = PathBuf::from(path);
+ asm_path.set_file_name(file_name);
+ asm_path.set_extension("asm");
+ write_file(&asm_path, build_asm(&compiled))?;
+ assemble(&asm_path, &PathBuf::from(file_name))?;
Ok(())
}
+
+fn build_asm(compiled: &Compiled) -> String {
+ let mut asm = String::from("format ELF64 executable\nentry _start\n\nsegment readable executable\n\n");
+
+ let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
+ for (op, file) in &compiled.validator.link_files {
+ if !compiled.called.contains(op) {
+ continue;
+ }
+ if !seen.insert(file.as_str()) {
+ continue;
+ }
+ let absolute = std::fs::canonicalize(file)
+ .map(|path| path.to_string_lossy().into_owned())
+ .unwrap_or_else(|_| file.clone());
+ asm.push_str(&format!("include \"{absolute}\"\n"));
+ }
+ if !seen.is_empty() {
+ asm.push('\n');
+ }
+
+ for code in &compiled.code {
+ asm.push_str(code);
+ asm.push('\n');
+ }
+ if !compiled.data.is_empty() {
+ asm.push_str(&compiled.data.join("\n"));
+ asm.push('\n');
+ }
+ asm
+}
+
pub fn compile_obj(
path: &str,
is_start: bool,
file_name: &str,
-) -> Result {
+) -> Result {
println!("{path}");
let module_source = file_system::read_file(path)?;
let parsed = parser(module_source)?;
let mut validator = Validator::default();
+ let mut code: Vec = Vec::new();
+ let mut data: Vec = Vec::new();
+ let mut called: std::collections::HashSet = std::collections::HashSet::new();
for module in parsed.modules {
let mut obj_path = PathBuf::from(path);
@@ -50,30 +94,44 @@ pub fn compile_obj(
&module,
)?;
- validator.functions.extend(compiled_context.functions);
- validator.variables.extend(compiled_context.variables);
- validator.constvars.extend(compiled_context.constvars);
- validator.link_files.extend(compiled_context.link_files);
+ validator.functions.extend(compiled_context.validator.functions);
+ validator
+ .variables
+ .extend(compiled_context.validator.variables);
+ validator
+ .constvars
+ .extend(compiled_context.validator.constvars);
+ validator
+ .link_files
+ .extend(compiled_context.validator.link_files);
+ code.extend(compiled_context.code);
+ data.extend(compiled_context.data);
+ called.extend(compiled_context.called);
}
let mut validated_program = validator.validate(parsed.ast)?;
- let mut transpiler = Transpiler::default();
validated_program.variables = validator.constvars.clone();
- let transpiled_code;
- if is_start {
- transpiled_code = transpiler.transpile_start(validated_program);
- } else {
- transpiled_code = transpiler.transpile_module(&file_name, validated_program);
- }
- let mut path = PathBuf::from(path);
- path.set_file_name(file_name);
- path.set_extension("il");
+ let externals: std::collections::HashSet = validator
+ .link_files
+ .iter()
+ .map(|(op, _)| op.clone())
+ .collect();
+
+ let mut transpiler = Transpiler::default();
+ let output = transpiler.transpile(file_name, validated_program, is_start, externals);
- write_file(&path, transpiled_code)?;
- let obj = compile_to_obj(path);
- validator.link_files.push(obj.to_str().unwrap().to_string());
+ code.push(output.code);
+ if !output.data.is_empty() {
+ data.push(output.data);
+ }
+ called.extend(output.called);
- return Ok(validator);
+ Ok(Compiled {
+ validator,
+ code,
+ data,
+ called,
+ })
}
#[cfg(test)]
diff --git a/compiler/src/pipline.rs b/compiler/src/pipline.rs
index 5f081cf..172e016 100644
--- a/compiler/src/pipline.rs
+++ b/compiler/src/pipline.rs
@@ -1,60 +1,19 @@
-use std::{ffi::OsString, path::PathBuf, process::Command};
+use std::{path::Path, process::Command};
-use parser::ParsedProgram;
-use transpiler::Transpiler;
-use validator::{ValidatedProgram, Validator, errors::ValidatorError};
-pub fn executer(program_name: &str, static_libs: Vec) {
- let output = Command::new("ld.lld")
- .args(static_libs)
- .args(["-o", program_name])
+use crate::errors::{BackendError, CompilerError};
+
+pub fn assemble(asm: &Path, binary: &Path) -> Result<(), CompilerError> {
+ let output = Command::new("fasm")
+ .arg(asm)
+ .arg(binary)
.output()
- .expect("Linker Error");
+ .map_err(|_| CompilerError::Backend(BackendError::Fasm))?;
if !output.status.success() {
eprintln!(
- "\x1b[31m[Linker Error]:\x1b[0m\n{}",
+ "\x1b[31m[Fasm Error]:\x1b[0m\n{}",
String::from_utf8_lossy(&output.stderr)
);
std::process::exit(output.status.code().unwrap_or(1));
}
+ Ok(())
}
-pub fn compile_to_obj(module: PathBuf) -> PathBuf {
- let mut s = module.to_path_buf();
- s.set_extension("s");
-
- let mut il = module.to_path_buf();
- il.set_extension("il");
-
- let mut o = module;
- o.set_extension("o");
-
- Command::new("qbe")
- .args(["-o"])
- .arg(&s)
- .arg(&il)
- .output()
- .expect("Problem oldu");
-
- Command::new("as")
- .arg(s)
- .arg("-o")
- .arg(&o)
- .output()
- .expect("Problem oldu");
- o
-}
-pub struct CompilerPipline(pub T);
-// impl CompilerPipline {
-// pub fn validate(self) -> Result, ValidatorError> {
-// let validator = validator::Validator::default();
-// for module in self.0.modules {
-// //TODO: We must finish this stuff.
-// }
-// let validated = validator.validate(self.0.ast)?;
-// Ok(CompilerPipline(validated))
-// }
-// }
-// impl CompilerPipline {
-// pub fn transpile(self) -> (validator::Validator, String) {
-// (self.0.0, Transpiler::default().transpile(self.0.1))
-// }
-// }
diff --git a/compiler/src/tests/compiler_test.rs b/compiler/src/tests/compiler_test.rs
index 58d2964..3abcf69 100644
--- a/compiler/src/tests/compiler_test.rs
+++ b/compiler/src/tests/compiler_test.rs
@@ -1,34 +1,9 @@
-use std::{path::PathBuf, process::Command};
-
-use file_system::write_file;
-use transpiler::Transpiler;
use which::which;
-use crate::{
- errors::{BackendError, CompilerError},
- parser,
-};
+use crate::errors::{BackendError, CompilerError};
#[test]
fn dependencies_test() -> Result<(), CompilerError> {
- which("qbe").map_err(|_| CompilerError::Backend(BackendError::Qbe))?;
- which("as").map_err(|_| CompilerError::Backend(BackendError::BinUtils))?;
- which("ld").map_err(|_| CompilerError::Backend(BackendError::BinUtils))?;
+ which("fasm").map_err(|_| CompilerError::Backend(BackendError::Fasm))?;
Ok(())
}
-// #[test]
-// fn compiler_exit_output_file() -> Result<(), CompilerError> {
-// let source = file_system::read_file("asda")?;
-// let parsed_program = parser(source)?;
-
-// let validator = validator::Validator::default();
-// let (context, program) = validator.validate(parsed_program)?;
-// let transpiled_code = Transpiler::default().transpile(program);
-
-// let main_file: PathBuf = PathBuf::from("main.ssa");
-// write_file(&main_file, transpiled_code)?;
-// let output = executer(context.link_files);
-// assert_eq!(output.status.code(), Some(50));
-// Ok(())
-// }
-//cargo test -- --test-threads=1
diff --git a/crates/parser/src/ast.rs b/crates/parser/src/ast.rs
index 55d69dc..cc4d9c4 100644
--- a/crates/parser/src/ast.rs
+++ b/crates/parser/src/ast.rs
@@ -119,7 +119,7 @@ pub enum Statement {
body: Vec,
},
Loop {
- body: Vec,
+ body: Vec,
},
ForLoop {
var_name: Atom,
diff --git a/crates/parser/src/binary_op.rs b/crates/parser/src/binary_op.rs
index f163a4c..566d565 100644
--- a/crates/parser/src/binary_op.rs
+++ b/crates/parser/src/binary_op.rs
@@ -112,11 +112,11 @@ fn parse_binary_op_with_precedence(
}
}
}
- return Ok(Expr::Call {
+ left = Expr::Call {
target: None,
name: Box::new(left),
args,
- });
+ };
}
loop {
let op = match tokens.peek() {
diff --git a/crates/parser/src/loop.rs b/crates/parser/src/loop.rs
index 0434c7f..5db06e7 100644
--- a/crates/parser/src/loop.rs
+++ b/crates/parser/src/loop.rs
@@ -1,6 +1,6 @@
use crate::{
ast::{Atom, Statement},
- binary_op::parse_expression,
+ condition::parse_block,
errors::ParserError,
expressions::parse_single_expr,
helpers::expect_token,
@@ -11,38 +11,8 @@ use tokenizer::{
};
pub fn parse_loop(tokens: &mut Tokens) -> Result {
tokens.next();
-
- let mut body = vec![];
-
- if let Some(token) = tokens.next()
- && matches!(
- token,
- SpannedToken {
- token: Token::Newline,
- ..
- }
- )
- && let Some(newline) = tokens.next()
- && matches!(
- newline,
- SpannedToken {
- token: Token::Newline,
- ..
- }
- )
- {
- while let Some(token) = tokens.peek()
- && !matches!(
- token,
- SpannedToken {
- token: Token::Dedent,
- ..
- }
- )
- {
- body.push(parse_expression(tokens)?);
- }
- }
+ expect_token(tokens, Token::Newline)?;
+ let body = parse_block(tokens)?;
Ok(Statement::Loop { body })
}
pub fn parse_for_loop(tokens: &mut Tokens) -> Result {
diff --git a/crates/parser/src/tests/loop.rs b/crates/parser/src/tests/loop.rs
index 948bbe6..52563ce 100644
--- a/crates/parser/src/tests/loop.rs
+++ b/crates/parser/src/tests/loop.rs
@@ -15,6 +15,7 @@ use crate::{
fn parse_loop_test() -> Result<(), ParserError> {
let mut tokens = create_tokens(vec![
Token::Loop,
+ Token::Newline,
Token::Indent,
Token::Identifier("Hello".to_string()),
Token::LParen,
@@ -23,14 +24,14 @@ fn parse_loop_test() -> Result<(), ParserError> {
let parseloop = parse_loop(&mut tokens)?;
assert_eq!(
Statement::Loop {
- body: vec![Expr::Call {
+ body: vec![Statement::Expr(Expr::Call {
target: None,
name: Box::new(Expr::VariableRef {
name: Atom::from("Hello"),
symbol: None
}),
args: vec![]
- }]
+ })]
},
parseloop
);
diff --git a/crates/tokenizer/src/lib.rs b/crates/tokenizer/src/lib.rs
index b8ff0ca..a402107 100644
--- a/crates/tokenizer/src/lib.rs
+++ b/crates/tokenizer/src/lib.rs
@@ -263,12 +263,25 @@ impl<'a> Lexer<'a> {
'%' => Ok(Token::Modulo),
'>' => Ok(Token::Greater),
'<' => Ok(Token::Less),
- '=' if self.chars.next() == Some('=') => Ok(Token::Equal),
+ '=' if self.chars.peek() == Some(&'=') => {
+ self.chars.next();
+ Ok(Token::Equal)
+ }
'=' => Ok(Token::Assign),
- '!' if self.chars.next() == Some('=') => Ok(Token::NotEqual),
+ '!' if self.chars.peek() == Some(&'=') => {
+ self.chars.next();
+ Ok(Token::NotEqual)
+ }
'!' => Ok(Token::Not),
- '&' if self.chars.peek() == Some(&'=') => Ok(Token::DoubleAnd),
+ '&' if self.chars.peek() == Some(&'&') => {
+ self.chars.next();
+ Ok(Token::And)
+ }
'&' => Ok(Token::And),
+ '|' if self.chars.peek() == Some(&'|') => {
+ self.chars.next();
+ Ok(Token::Or)
+ }
'|' => Ok(Token::Or),
_ => Err(LexerError::UnexpectedToken(
SourceSpan {
@@ -332,7 +345,7 @@ impl<'a> Lexer<'a> {
Some('[') => self.consume(Token::ListStart),
Some(']') => self.consume(Token::ListEnd),
Some('=') | Some('/') | Some('*') | Some('%') | Some('^') | Some('>') | Some('<')
- | Some('+') | Some('-') | Some('!') => self.read_operator(),
+ | Some('+') | Some('-') | Some('!') | Some('&') | Some('|') => self.read_operator(),
Some('0'..='9') => self.read_number(),
Some('\'') | Some('"') => self.read_string(),
Some(_) => self.read_word(),
diff --git a/crates/tokenizer/src/tests.rs b/crates/tokenizer/src/tests.rs
index f96c964..08ec340 100644
--- a/crates/tokenizer/src/tests.rs
+++ b/crates/tokenizer/src/tests.rs
@@ -265,7 +265,7 @@ fn test_keyword_else() {
#[test]
fn test_keyword_function_def() {
- assert_eq!(tokenize("func"), vec![Token::FunctionDef]);
+ assert_eq!(tokenize("op"), vec![Token::FunctionDef]);
}
#[test]
@@ -275,7 +275,7 @@ fn test_keyword_function_type() {
#[test]
fn overal() {
assert_eq!(
- tokenize("@link(\"Hello\")\nfunc Hello():void "),
+ tokenize("@link(\"Hello\")\nop Hello():void "),
vec![
Token::At,
Token::Identifier("link".to_string()),
diff --git a/crates/transpiler/src/lib.rs b/crates/transpiler/src/lib.rs
index 2f9f31b..dda632f 100644
--- a/crates/transpiler/src/lib.rs
+++ b/crates/transpiler/src/lib.rs
@@ -1,227 +1,399 @@
-use parser::{self, shared_ast::Type};
-use std::{collections::HashMap, io::Write};
-use validator::ast::{
- Ast::{self},
- Expr, Program,
-};
-#[derive(Default)]
+use parser::ast::Operation;
+use std::collections::{HashMap, HashSet};
+use validator::ast::{Ast, Else, Expr, Function, IF, Program};
+
+const ARG_REGS: [&str; 6] = ["rdi", "rsi", "rdx", "rcx", "r8", "r9"];
+
+#[derive(Debug, Default)]
+pub struct ModuleOutput {
+ pub code: String,
+ pub data: String,
+ pub called: HashSet,
+}
+
+#[derive(Debug, Default)]
pub struct Transpiler {
- variables: HashMap,
+ prefix: String,
+ label_id: u64,
+ slots: HashMap,
+ constvars: HashMap,
data: Vec,
- stack: Vec,
- labels: String,
- conditions: u64,
+ string_labels: HashMap,
+ called: HashSet,
+ externals: HashSet,
+ breaks: Vec,
+ continues: Vec,
+ in_function: bool,
+}
+
+fn frame_bytes(slots: usize) -> usize {
+ (slots * 8).div_ceil(16) * 16
+}
+
+fn unescape(s: &str) -> String {
+ let mut out = String::with_capacity(s.len());
+ let mut chars = s.chars();
+ while let Some(c) = chars.next() {
+ if c != '\\' {
+ out.push(c);
+ continue;
+ }
+ match chars.next() {
+ Some('n') => out.push('\n'),
+ Some('t') => out.push('\t'),
+ Some('r') => out.push('\r'),
+ Some('0') => out.push('\0'),
+ Some('\\') => out.push('\\'),
+ Some(other) => {
+ out.push('\\');
+ out.push(other);
+ }
+ None => out.push('\\'),
+ }
+ }
+ out
+}
+
+fn fasm_string(s: &str) -> String {
+ let mut parts: Vec = Vec::new();
+ let mut run = String::new();
+ for &b in s.as_bytes() {
+ if (0x20..0x7f).contains(&b) {
+ if b == b'"' {
+ run.push_str("\"\"");
+ } else {
+ run.push(b as char);
+ }
+ } else {
+ if !run.is_empty() {
+ parts.push(format!("\"{run}\""));
+ run.clear();
+ }
+ parts.push(b.to_string());
+ }
+ }
+ if !run.is_empty() {
+ parts.push(format!("\"{run}\""));
+ }
+ parts.push(String::from("0"));
+ format!("db {}", parts.join(","))
}
+
impl Transpiler {
- fn push_temp_stack(&mut self, expr: Expr) -> String {
- match expr {
- Expr::VariableRef { name, symbol } => {
- match symbol.typ {
- Type::String(_) => {
- return format!("l ${name}");
- }
- Type::Natural => {
- panic!();
- }
- Type::Integer => {
- return format!("l ${name}");
- }
- Type::User(_) => {
- if let Some(val) = self.variables.get(&name.to_string()) {
- return format!("l {}", val);
- }
- }
- other => todo!("{other} is not implemented yet"),
+ fn fresh(&mut self, tag: &str) -> String {
+ self.label_id += 1;
+ format!(".{tag}{}", self.label_id)
+ }
+
+ fn slot(&mut self, name: &str) -> usize {
+ if let Some(&index) = self.slots.get(name) {
+ return index;
+ }
+ let index = self.slots.len();
+ self.slots.insert(name.to_string(), index);
+ index
+ }
+
+ fn intern_string(&mut self, s: &str) -> String {
+ if let Some(label) = self.string_labels.get(s) {
+ return label.clone();
+ }
+ let label = format!("{}_str{}", self.prefix, self.data.len());
+ self.data.push(format!("{label} {}", fasm_string(&unescape(s))));
+ self.string_labels.insert(s.to_string(), label.clone());
+ label
+ }
+
+ fn reset_frame(&mut self) {
+ self.slots.clear();
+ self.breaks.clear();
+ self.continues.clear();
+ }
+
+ fn expr(&mut self, out: &mut String, e: &Expr) {
+ match e {
+ Expr::Void => {}
+ Expr::Number(num) => out.push_str(&format!(" mov rax, {num}\n")),
+ Expr::Bool(b) => out.push_str(&format!(" mov rax, {}\n", i32::from(*b))),
+ Expr::Char(c) => out.push_str(&format!(" mov rax, {}\n", *c as u32)),
+ Expr::String(s) => {
+ let label = self.intern_string(s);
+ out.push_str(&format!(" lea rax, [{label}]\n"));
+ }
+ Expr::VariableRef { name, .. } => {
+ if let Some(&index) = self.slots.get(name) {
+ out.push_str(&format!(" mov rax, [rbp-{}]\n", (index + 1) * 8));
+ } else if let Some(value) = self.constvars.get(name).cloned() {
+ self.expr(out, &value);
+ } else {
+ panic!(
+ "variable `{name}` is not accessible here; \
+ global variables cannot be referenced inside functions"
+ );
}
- format!("w %{name}")
}
- Expr::Number(num) => {
- format!("l {num}")
+ Expr::BinaryOp { left, right, op, .. } => self.binary(out, left, right, *op),
+ Expr::Call { name, args, .. } => self.call(out, name, args),
+ Expr::Return(inner) => {
+ if !self.in_function {
+ panic!("`return` outside of a function");
+ }
+ self.expr(out, inner);
+ out.push_str(" leave\n ret\n");
}
- Expr::String(str) => {
- self.data.push(format!(
- "data $str{} ={{ b \"{str}\", b 0 }} ",
- self.data.len()
- ));
- format!("l $str{}", self.data.len() - 1)
+ Expr::Break => {
+ let target = self
+ .breaks
+ .last()
+ .cloned()
+ .unwrap_or_else(|| panic!("`break` outside of a loop"));
+ out.push_str(&format!(" jmp {target}\n"));
}
- Expr::BinaryOp {
- left,
- right,
- op,
- return_type,
- } => {
- match op {
- parser::ast::Operation::Add => self.stack.push(format!(
- "%bin{} = w add {},{}",
- self.stack.len(),
- *left,
- *right
- )),
- parser::ast::Operation::Greater => self.stack.push(format!(
- "%bin{} = w cgt {},{}",
- self.stack.len(),
- *left,
- *right
- )),
- parser::ast::Operation::Not => self.stack.push(format!(
- "%bin{} = w ceqw {},{}",
- self.stack.len(),
- *left,
- *right //Bug
- )),
-
- parser::ast::Operation::Subtract => self.stack.push(format!(
- "%bin{} = w sub {},{}",
- self.stack.len(),
- *left,
- *right
- )),
- parser::ast::Operation::Multiply => self.stack.push(format!(
- "%bin{} = w mul {},{}",
- self.stack.len(),
- *left,
- *right
- )),
- parser::ast::Operation::Divide => self.stack.push(format!(
- "%bin{} = w div {},{}",
- self.stack.len(),
- *left,
- *right
- )),
-
- parser::ast::Operation::Equal => self.stack.push(format!(
- "%bin{} = w ceqd {},{}",
- self.stack.len(),
- *left,
- *right
- )),
-
- parser::ast::Operation::NotEqual => self.stack.push(format!(
- "%bin{} = w cnew {},{}",
- self.stack.len(),
- *left,
- *right
- )),
- parser::ast::Operation::GreaterEqual => self.stack.push(format!(
- "%bin{} = w csgew {},{}",
- self.stack.len(),
- *left,
- *right
- )),
- other => todo!("This is not implemented yet {other:?}"),
- };
- format!("%bin{}", self.stack.len() - 1)
+ Expr::Continue => {
+ let target = self
+ .continues
+ .last()
+ .cloned()
+ .unwrap_or_else(|| panic!("`continue` outside of a loop"));
+ out.push_str(&format!(" jmp {target}\n"));
}
- Expr::Void => String::from(""),
+ Expr::Float(_) => todo!("float is not implemented yet"),
+ Expr::TemplateString(_) => todo!("template string is not implemented yet"),
+ Expr::List(_) => todo!("list is not implemented yet"),
+ }
+ }
- other => {
- todo!("{other:?} there is not complated yet. acutally, i dont know what to do ")
+ fn binary(&mut self, out: &mut String, left: &Expr, right: &Expr, op: Operation) {
+ if matches!(left, Expr::Void) {
+ match op {
+ Operation::Not => {
+ self.expr(out, right);
+ out.push_str(" test rax, rax\n sete al\n movzx eax, al\n");
+ }
+ Operation::Subtract => {
+ self.expr(out, right);
+ out.push_str(" neg rax\n");
+ }
+ other => todo!("unary operator {other:?} is not implemented yet"),
}
+ return;
+ }
+ self.expr(out, left);
+ out.push_str(" push rax\n");
+ self.expr(out, right);
+ out.push_str(" mov rcx, rax\n pop rax\n");
+ match op {
+ Operation::Add => out.push_str(" add rax, rcx\n"),
+ Operation::Subtract => out.push_str(" sub rax, rcx\n"),
+ Operation::Multiply => out.push_str(" imul rax, rcx\n"),
+ Operation::Divide => out.push_str(" cqo\n idiv rcx\n"),
+ Operation::Modulo => out.push_str(" cqo\n idiv rcx\n mov rax, rdx\n"),
+ Operation::Equal => setcc(out, "sete"),
+ Operation::NotEqual => setcc(out, "setne"),
+ Operation::Greater => setcc(out, "setg"),
+ Operation::GreaterEqual => setcc(out, "setge"),
+ Operation::Less => setcc(out, "setl"),
+ Operation::LessEqual => setcc(out, "setle"),
+ Operation::And => out.push_str(" and rax, rcx\n"),
+ Operation::Or => out.push_str(" or rax, rcx\n"),
+ Operation::Not => todo!("unary not cannot be applied here"),
}
}
- fn expr_transpiler(&mut self, stream: &mut String, expr: Expr) {
- if let Expr::Call {
- target, args, name, ..
- } = expr
- {
- stream.push_str(&format!("call ${name}("));
- let argslen = args.len();
-
- for (index, arg) in args.into_iter().enumerate() {
- let variable = self.push_temp_stack(arg);
- stream.push_str(&variable);
- if index < argslen - 1 {
- stream.push(',');
- }
+ fn call(&mut self, out: &mut String, name: &Expr, args: &[Expr]) {
+ let func = match name {
+ Expr::VariableRef { name, .. } => name.clone(),
+ other => panic!("cannot call {other:?}"),
+ };
+ self.called.insert(func.clone());
+ let target = if self.externals.contains(&func) {
+ func
+ } else {
+ format!("azfn_{func}")
+ };
+
+ let count = args.len();
+ let stack_args = count.saturating_sub(6);
+ let pad = stack_args & 1;
+
+ if pad == 1 {
+ out.push_str(" push rax\n");
+ }
+ if stack_args == 0 {
+ for arg in args {
+ self.expr(out, arg);
+ out.push_str(" push rax\n");
+ }
+ for i in (0..count).rev() {
+ out.push_str(&format!(" pop {}\n", ARG_REGS[i]));
}
- stream.push(')');
- stream.push('\n');
} else {
- stream.push_str(&self.push_temp_stack(expr));
+ for arg in args.iter().rev() {
+ self.expr(out, arg);
+ out.push_str(" push rax\n");
+ }
+ for reg in ARG_REGS {
+ out.push_str(&format!(" pop {reg}\n"));
+ }
+ }
+ out.push_str(&format!(" call {target}\n"));
+ if stack_args > 0 {
+ out.push_str(&format!(" add rsp, {}\n", (stack_args + pad) * 8));
}
}
- pub fn transpile_body(&mut self, exprstream: &mut String, body: Vec) {
+
+ fn condition(&mut self, out: &mut String, main: IF, elif: Vec, other: Option) {
+ let end = self.fresh("end");
+ let mut next = self.fresh("br");
+
+ self.expr(out, &main.condition);
+ out.push_str(&format!(" test rax, rax\n jz {next}\n"));
+ self.transpile_body(out, main.body);
+ out.push_str(&format!(" jmp {end}\n{next}:\n"));
+
+ for branch in elif {
+ next = self.fresh("br");
+ self.expr(out, &branch.condition);
+ out.push_str(&format!(" test rax, rax\n jz {next}\n"));
+ self.transpile_body(out, branch.body);
+ out.push_str(&format!(" jmp {end}\n{next}:\n"));
+ }
+
+ if let Some(els) = other {
+ self.transpile_body(out, els.body);
+ }
+ out.push_str(&format!("{end}:\n"));
+ }
+
+ pub fn transpile_body(&mut self, out: &mut String, body: Vec) {
for ast in body {
match ast {
- Ast::Expr(expr) => self.expr_transpiler(exprstream, expr),
- Ast::Condition { main, elif, other } => {
- self.conditions += 1;
- let condition_num = self.conditions;
- let mut condition = String::new();
- self.expr_transpiler(&mut condition, *main.condition);
- exprstream.push_str(&format!(
- "jnz {condition}, @true{condition_num}, @false{condition_num}\n"
- ));
- exprstream.push_str(&format!("@true{condition_num}\n"));
- self.transpile_body(exprstream, main.body);
- exprstream.push_str(&format!(
- "jmp @continue{condition_num}\n@false{condition_num}\n"
- ));
- if let Some(els) = other {
- self.transpile_body(exprstream, els.body);
- }
- exprstream.push_str(&format!(
- "jmp @continue{condition_num}\n@continue{condition_num}\n"
- ));
+ Ast::Expr(expr) => self.expr(out, &expr),
+ Ast::Decl { name, value, .. } => {
+ self.expr(out, &value);
+ let index = self.slot(&name);
+ out.push_str(&format!(" mov [rbp-{}], rax\n", (index + 1) * 8));
+ }
+ Ast::Assignment { name, value } => {
+ self.expr(out, &value);
+ let index = match self.slots.get(&name) {
+ Some(&index) => index,
+ None => panic!(
+ "variable `{name}` is not accessible here; \
+ global variables cannot be referenced inside functions"
+ ),
+ };
+ out.push_str(&format!(" mov [rbp-{}], rax\n", (index + 1) * 8));
+ }
+ Ast::Condition { main, elif, other } => self.condition(out, main, elif, other),
+ Ast::While { condition, body } => {
+ let top = self.fresh("w");
+ let end = self.fresh("we");
+ self.breaks.push(end.clone());
+ self.continues.push(top.clone());
+ out.push_str(&format!("{top}:\n"));
+ self.expr(out, &condition);
+ out.push_str(&format!(" test rax, rax\n jz {end}\n"));
+ self.transpile_body(out, body);
+ out.push_str(&format!(" jmp {top}\n{end}:\n"));
+ self.breaks.pop();
+ self.continues.pop();
}
Ast::Loop { body } => {
- exprstream.push_str(&format!("@loop1\n"));
- exprstream.push_str(&String::from("jmp @loop1\n"));
- exprstream.push_str(&String::from("@continue2\n"));
+ let top = self.fresh("l");
+ let end = self.fresh("le");
+ self.breaks.push(end.clone());
+ self.continues.push(top.clone());
+ out.push_str(&format!("{top}:\n"));
+ self.transpile_body(out, body);
+ out.push_str(&format!(" jmp {top}\n{end}:\n"));
+ self.breaks.pop();
+ self.continues.pop();
}
- Ast::Decl {
- name,
- typ,
- is_mutable,
- value,
- } => match typ {
- Type::String(strenum) => {
- self.data
- .push(format!("data ${name} = {{ b {value}, b 0 }}\n"));
- }
- other => todo!("{other:?} Not implemented yet"),
- },
- _ => {}
+ Ast::Exit(expr) => {
+ self.expr(out, &expr);
+ out.push_str(" mov edi, eax\n mov eax, 60\n syscall\n");
+ }
+ Ast::Enum { .. } => {}
}
}
}
- pub fn transpile_start(&mut self, program: Program) -> String {
- self.variables = program.variables;
- let mut exprstream = String::new();
- self.transpile_body(&mut exprstream, program.expressions);
- format!(
- "{}
-export function w $_start() {{
-@start
-{}
-{exprstream}
-call $exit(w 0)
-ret
-}}
-",
- self.data.join("\n"),
- self.stack.join(""),
- )
+ fn function(&mut self, out: &mut String, function: Function) {
+ self.reset_frame();
+ self.in_function = true;
+
+ let mut params = String::new();
+ for (index, param) in function.params.iter().enumerate() {
+ let slot = (self.slot(param.name.as_ref()) + 1) * 8;
+ if index < ARG_REGS.len() {
+ params.push_str(&format!(" mov [rbp-{slot}], {}\n", ARG_REGS[index]));
+ } else {
+ params.push_str(&format!(
+ " mov rax, [rbp+{}]\n mov [rbp-{slot}], rax\n",
+ 16 + (index - ARG_REGS.len()) * 8
+ ));
+ }
+ }
+
+ let mut body = String::new();
+ self.transpile_body(&mut body, function.body);
+ let frame = frame_bytes(self.slots.len());
+
+ out.push_str(&format!(
+ "azfn_{}:\n push rbp\n mov rbp, rsp\n",
+ function.name
+ ));
+ if frame > 0 {
+ out.push_str(&format!(" sub rsp, {frame}\n"));
+ }
+ out.push_str(¶ms);
+ out.push_str(&body);
+ if !body.ends_with(" ret\n") {
+ out.push_str(" leave\n ret\n");
+ }
+
+ self.in_function = false;
}
- pub fn transpile_module(&mut self, module: &str, program: Program) -> String {
- self.variables = program.variables;
- let mut exprstream = String::new();
- self.transpile_body(&mut exprstream, program.expressions);
- format!(
- "{}
-export function w ${module}() {{
-@start
-{}
-{exprstream}
-call $exit(w 0)
-ret
-}}
-",
- self.data.join("\n"),
- self.stack.join(""),
- )
+
+ pub fn transpile(
+ &mut self,
+ prefix: &str,
+ program: Program,
+ is_start: bool,
+ externals: HashSet,
+ ) -> ModuleOutput {
+ self.prefix = prefix.to_string();
+ self.constvars = program.variables;
+ self.externals = externals;
+ let mut code = String::new();
+
+ for function in program.functions {
+ self.function(&mut code, function);
+ }
+
+ if is_start {
+ self.reset_frame();
+ self.in_function = false;
+ let mut body = String::new();
+ self.transpile_body(&mut body, program.expressions);
+ let frame = frame_bytes(self.slots.len()) + 8;
+ code.push_str("_start:\n push rbp\n mov rbp, rsp\n");
+ code.push_str(&format!(" sub rsp, {frame}\n"));
+ code.push_str(&body);
+ code.push_str(" xor edi, edi\n mov eax, 60\n syscall\n");
+ }
+
+ ModuleOutput {
+ code,
+ data: self.data.join("\n"),
+ called: std::mem::take(&mut self.called),
+ }
}
}
+
+fn setcc(out: &mut String, cc: &str) {
+ out.push_str(&format!(
+ " cmp rax, rcx\n {cc} al\n movzx eax, al\n"
+ ));
+}
diff --git a/crates/validator/src/lib.rs b/crates/validator/src/lib.rs
index b96d08b..9373693 100644
--- a/crates/validator/src/lib.rs
+++ b/crates/validator/src/lib.rs
@@ -34,7 +34,7 @@ pub struct MethodInfo {
pub struct Validator {
pub functions: HashMap,
pub variables: Vec>,
- pub link_files: Vec,
+ pub link_files: Vec<(String, String)>,
pub enums: HashMap>,
pub constvars: HashMap,
}
@@ -75,7 +75,7 @@ impl Validator {
return Err(ValidatorError::FunctionAlreadyDefined(namestr));
}
- self.link_files.push(library.to_string());
+ self.link_files.push((namestr.clone(), library.to_string()));
self.functions.insert(
namestr,
FunctionInfo {
diff --git a/crates/validator/src/tests/assignment.rs b/crates/validator/src/tests/assignment.rs
index a7ec464..350a5cf 100644
--- a/crates/validator/src/tests/assignment.rs
+++ b/crates/validator/src/tests/assignment.rs
@@ -22,7 +22,7 @@ fn test_assignment_success() {
let mut validator = Validator::default();
validator.variables.push(HashMap::new());
validator.declare_variable(
- "x".to_string(),
+ Atom::from("x"),
Symbol {
typ: Type::Integer,
is_mutable: true,
@@ -38,7 +38,7 @@ fn test_assignment_success() {
.variables
.last()
.expect("variable stack should not be empty")
- .get("x")
+ .get(&Atom::from("x"))
.expect("symbol 'x' should exist in scope");
assert!(
@@ -52,7 +52,7 @@ fn test_assignment_to_immutable() {
let mut validator = Validator::default();
validator.variables.push(HashMap::new());
validator.declare_variable(
- "x".to_string(),
+ Atom::from("x"),
Symbol {
typ: Type::Integer,
is_mutable: false,
@@ -70,7 +70,7 @@ fn test_assignment_type_mismatch() {
let mut validator = Validator::default();
validator.variables.push(HashMap::new());
validator.declare_variable(
- "x".to_string(),
+ Atom::from("x"),
Symbol {
typ: Type::Integer,
is_mutable: true,
diff --git a/crates/validator/src/tests/binary_op.rs b/crates/validator/src/tests/binary_op.rs
index 311d2b6..2afe213 100644
--- a/crates/validator/src/tests/binary_op.rs
+++ b/crates/validator/src/tests/binary_op.rs
@@ -11,7 +11,7 @@ fn test_binary_op_add_integers() {
right: Box::new(Expr::Number(3)),
op: Operation::Add,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -33,7 +33,7 @@ fn test_binary_op_add_strings() {
right: Box::new(Expr::String(Atom::from("Yupiter"))),
op: Operation::Add,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -56,7 +56,7 @@ fn test_binary_op_subtract_integers() {
right: Box::new(Expr::Number(4)),
op: Operation::Subtract,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -78,7 +78,7 @@ fn test_binary_op_multiply_floats() {
right: Box::new(Expr::Float(3.0)),
op: Operation::Multiply,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -100,7 +100,7 @@ fn test_binary_op_mixed_int_float() {
right: Box::new(Expr::Float(2.0)),
op: Operation::Add,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -122,7 +122,7 @@ fn test_binary_op_divide_integers() {
right: Box::new(Expr::Number(3)),
op: Operation::Divide,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -144,7 +144,7 @@ fn test_binary_op_equal_comparison() {
right: Box::new(Expr::Number(5)),
op: Operation::Equal,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -166,7 +166,7 @@ fn test_binary_op_not_equal_comparison() {
right: Box::new(Expr::Number(3)),
op: Operation::NotEqual,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -188,7 +188,7 @@ fn test_binary_op_greater_comparison() {
right: Box::new(Expr::Number(5)),
op: Operation::Greater,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -210,7 +210,7 @@ fn test_binary_op_less_equal_comparison() {
right: Box::new(Expr::Number(3)),
op: Operation::LessEqual,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -232,7 +232,7 @@ fn test_binary_op_logical_and() {
right: Box::new(Expr::Bool(false)),
op: Operation::And,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -254,7 +254,7 @@ fn test_binary_op_logical_or() {
right: Box::new(Expr::Bool(false)),
op: Operation::Or,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
@@ -276,7 +276,7 @@ fn test_binary_op_modulo() {
right: Box::new(Expr::Number(3)),
op: Operation::Modulo,
});
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(vec![stmt])
.expect("valid program should not fail");
diff --git a/crates/validator/src/tests/function_call.rs b/crates/validator/src/tests/function_call.rs
index 890be5b..dbeee09 100644
--- a/crates/validator/src/tests/function_call.rs
+++ b/crates/validator/src/tests/function_call.rs
@@ -107,7 +107,7 @@ fn test_function_call_return_type_in_result() {
args: vec![],
}),
];
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(stmts)
.expect("should validate");
let returned_type = match &program.expressions[0] {
@@ -130,7 +130,7 @@ fn test_function_call_return_type_bool() {
args: vec![],
}),
];
- let (_validator, program) = Validator::default()
+ let program = Validator::default()
.validate(stmts)
.expect("should validate");
let returned_type = match &program.expressions[0] {
@@ -400,10 +400,12 @@ fn test_external_func_link_name_passed_through() {
args: vec![Expr::Number(42)],
}),
];
- let (_validator, program) = Validator::default()
- .validate(stmts)
- .expect("should validate with link_name");
- assert_eq!(program.external_functions.len(), 1);
+ let mut validator = Validator::default();
+ let _program = validator.validate(stmts).expect("should validate with link");
+ assert_eq!(
+ validator.link_files,
+ vec![("print".to_string(), "../build/printlib.so".to_string())]
+ );
}
#[test]
@@ -429,8 +431,12 @@ fn test_external_func_link_name_none_when_omitted() {
args: vec![Expr::Number(42)],
}),
];
- let (_validator, program) = Validator::default()
+ let mut validator = Validator::default();
+ let _program = validator
.validate(stmts)
- .expect("should validate without link_name");
- assert_eq!(program.external_functions.len(), 1);
+ .expect("should validate without link");
+ assert_eq!(
+ validator.link_files,
+ vec![("print".to_string(), "../build/printlib.so".to_string())]
+ );
}
diff --git a/crates/validator/src/tests/function_def.rs b/crates/validator/src/tests/function_def.rs
index 07365e5..c2c443d 100644
--- a/crates/validator/src/tests/function_def.rs
+++ b/crates/validator/src/tests/function_def.rs
@@ -212,18 +212,21 @@ fn test_function_decl_overwrites_duplicate() {
make_func("dup", Type::Bool, vec![], vec![]),
];
- validator.function_decl(&stmts);
-
- let info = validator.functions.get("dup").expect("dup should be registered");
- assert_eq!(info.return_type, Type::Bool);
+ let result = validator.function_decl(&stmts);
+ assert!(matches!(
+ result,
+ Err(crate::errors::ValidatorError::FunctionAlreadyDefined(_))
+ ));
}
#[test]
fn test_function_decl_returns_self() {
let mut validator = Validator::default();
let stmt = make_func("f", Type::Integer, vec![], vec![]);
- let result = validator.function_decl(&vec![stmt]);
- assert_eq!(result as *mut Validator, &mut validator as *mut Validator);
+ let result = validator
+ .function_decl(&vec![stmt])
+ .expect("function_decl should succeed") as *mut Validator;
+ assert_eq!(result, &mut validator as *mut Validator);
}
#[test]
@@ -268,7 +271,8 @@ fn test_validate_function_def_preserves_functions_in_result() {
make_func("foo", Type::Integer, vec![], vec![]),
make_func("bar", Type::Bool, vec![], vec![]),
];
- let (validator, _program) = Validator::default().validate(stmts).expect("should validate");
+ let mut validator = Validator::default();
+ let _program = validator.validate(stmts).expect("should validate");
assert!(validator.functions.contains_key("foo"));
assert!(validator.functions.contains_key("bar"));
@@ -291,7 +295,8 @@ fn test_validate_function_def_with_complex_return_type() {
vec![],
vec![],
)];
- let (validator, _program) = Validator::default().validate(stmts).expect("should validate");
+ let mut validator = Validator::default();
+ let _program = validator.validate(stmts).expect("should validate");
let info = validator.functions.get("get_list").unwrap();
assert_eq!(
diff --git a/crates/validator/src/tests/variable_decl.rs b/crates/validator/src/tests/variable_decl.rs
index c48ae82..68aff8c 100644
--- a/crates/validator/src/tests/variable_decl.rs
+++ b/crates/validator/src/tests/variable_decl.rs
@@ -29,7 +29,7 @@ fn test_variable_decl_integer() {
);
assert!(result.is_ok());
- let sym = validator.lookup_variable("x").unwrap();
+ let sym = validator.lookup_variable(&Atom::from("x")).unwrap();
assert_eq!(sym.typ, Type::Integer);
assert!(!sym.is_used);
assert!(!sym.is_mutable);
@@ -39,7 +39,7 @@ fn test_variable_decl_integer() {
fn test_variable_decl_already_declared() {
let mut validator = setup_validator();
validator.declare_variable(
- "x".to_string(),
+ Atom::from("x"),
Symbol {
typ: Type::Integer,
is_mutable: false,
@@ -71,7 +71,7 @@ fn test_string_decl_literal() {
);
assert!(result.is_ok());
- let sym = validator.lookup_variable("s").unwrap();
+ let sym = validator.lookup_variable(&Atom::from("s")).unwrap();
assert_eq!(sym.typ, Type::String(StringEnum::LiteralString));
}
@@ -87,7 +87,7 @@ fn test_string_decl_dynamic() {
);
assert!(result.is_ok());
- let sym = validator.lookup_variable("s").unwrap();
+ let sym = validator.lookup_variable(&Atom::from("s")).unwrap();
assert_eq!(sym.typ, Type::String(StringEnum::DynamicString));
}
@@ -115,7 +115,7 @@ fn test_string_decl_type_mismatch_int() {
fn test_variable_decl_already_declared_string() {
let mut validator = setup_validator();
validator.declare_variable(
- "s".to_string(),
+ Atom::from("s"),
Symbol {
typ: Type::String(StringEnum::LiteralString),
is_mutable: false,
@@ -199,7 +199,7 @@ fn test_decl_any() {
);
assert!(result.is_ok());
- let sym = validator.lookup_variable("a").unwrap();
+ let sym = validator.lookup_variable(&Atom::from("a")).unwrap();
assert_eq!(sym.typ, Type::Integer);
}
@@ -215,7 +215,7 @@ fn test_decl_any_with_string() {
);
assert!(result.is_ok());
- let sym = validator.lookup_variable("a").unwrap();
+ let sym = validator.lookup_variable(&Atom::from("a")).unwrap();
assert_eq!(sym.typ, Type::String(StringEnum::LiteralString));
}
@@ -395,7 +395,7 @@ fn test_array_decl_integer() {
);
assert!(result.is_ok());
- let sym = validator.lookup_variable("arr").unwrap();
+ let sym = validator.lookup_variable(&Atom::from("arr")).unwrap();
assert_eq!(sym.typ, Type::Array(Box::new(Type::Integer)));
}
@@ -411,7 +411,7 @@ fn test_array_decl_empty() {
);
assert!(result.is_ok());
- let sym = validator.lookup_variable("arr").unwrap();
+ let sym = validator.lookup_variable(&Atom::from("arr")).unwrap();
assert_eq!(sym.typ, Type::Array(Box::new(Type::Any)));
}
diff --git a/crates/validator/src/tests/while_loop.rs b/crates/validator/src/tests/while_loop.rs
index 939d67b..bdfaaa4 100644
--- a/crates/validator/src/tests/while_loop.rs
+++ b/crates/validator/src/tests/while_loop.rs
@@ -9,7 +9,7 @@ mod tests {
#[test]
fn test_while_loop_valid_bool_condition() {
- let validator = Validator::default();
+ let mut validator = Validator::default();
let program = vec![Statement::While {
condition: Box::new(Expr::Bool(true)),
body: vec![Statement::Expr(Expr::Number(1))],
@@ -20,7 +20,7 @@ mod tests {
#[test]
fn test_while_loop_invalid_condition_type() {
- let validator = Validator::default();
+ let mut validator = Validator::default();
let program = vec![Statement::While {
condition: Box::new(Expr::Number(42)),
body: vec![],
@@ -34,7 +34,7 @@ mod tests {
#[test]
fn test_while_loop_body_with_decl() {
- let validator = Validator::default();
+ let mut validator = Validator::default();
let program = vec![
Statement::Decl {
name: Atom::from("x"),
diff --git a/crates/validator/src/validate.rs b/crates/validator/src/validate.rs
index df86ccb..3bb9ba2 100644
--- a/crates/validator/src/validate.rs
+++ b/crates/validator/src/validate.rs
@@ -109,7 +109,15 @@ pub fn validate_statement(stmt: Statement, ctx: &mut Validator) -> Result Ok(Ast::Loop { body: vec![] }),
+ Statement::Loop { body } => {
+ let mut validated_body = Vec::new();
+ for stmt in body {
+ validated_body.push(validate_statement(stmt, ctx)?);
+ }
+ Ok(Ast::Loop {
+ body: validated_body,
+ })
+ }
Statement::FunctionDef { .. }
| Statement::EnumDecl { .. }
| Statement::StructDef { .. }
diff --git a/readme.md b/readme.md
index 387e0c6..aa12352 100644
--- a/readme.md
+++ b/readme.md
@@ -14,7 +14,7 @@
**AzLang** provides an approachable, understandable and performant coding experience for everyone — from beginners to professional developers.
-AzLang is a **compiled** language. The source is parsed into an AST, semantically validated, and then lowered through a dedicated backend. There is no runtime interpreter for `.az` scripts.
+AzLang is a **compiled** language. The source is parsed into an AST, semantically validated, lowered to a single fasm assembly file, and assembled into a native executable in one pass. There is no runtime interpreter for `.az` scripts.
---
@@ -71,7 +71,7 @@ a = 2
const str b = "Hi"
-func add(a: int, b: int): int
+op add(a: int, b: int): int
return a + b
print(add(1, 2))
@@ -87,6 +87,5 @@ cargo build --release
## Dependencies
-- [QBE](https://c9x.me/compile/) — afterburner backend / intermediate representation compiler
-- **Linux** — `binutils` (assembler) and `ld.lld` (linker)
-- **Windows** — an assembler (e.g. MASM or NASM) and `lld.link` (linker)
+- [fasm](https://flatassembler.net/) — the flat assembler; AzLang emits one `.asm` file and fasm turns it directly into a standalone ELF executable (assembling and linking in the same pass)
+- **Linux x86_64** — nothing else: no C runtime, no `binutils`, no `ld`, no LLVM/QBE
diff --git a/sdk b/sdk
index 5a78104..273f2b0 160000
--- a/sdk
+++ b/sdk
@@ -1 +1 @@
-Subproject commit 5a781044b530ed2c8fd389b5aa8e59ed4b14f392
+Subproject commit 273f2b04fa941748ec7638a9505d98880869942e