From 3ffe58d049b65631c49680a707e1a38b48a816c3 Mon Sep 17 00:00:00 2001 From: Prestgg1 Date: Wed, 7 Oct 2026 12:10:51 +0400 Subject: [PATCH] feat: replace QBE/as/ld.lld backend with single-pass fasm pipeline Compiler: - transpiler: emit fasm x86_64 assembly directly (stack-slot frame model, System V call ABI, azfn_ label prefix, string data dedupe, fasm local branch labels, \\n/\\t/\\r/\\0/\\\\ escapes resolved at emission) - validator: link_files is now (operation, source path); only invoked @link operations are included, each path exactly once - pipline: single fasm invocation, BackendError::Fasm; qbe/as/ld.lld, .il/.ssa/.o intermediate stages and the external linker are gone - compiler: builds one .asm (header, filtered includes, code, data) and fasm assembles+links it straight into a standalone ELF Parser/tokenizer fixes found by e2e: - &&/|| dispatch (previously an infinite loop in read_operator) - '='/'!' peek guards (!flag parsed the identifier away) - binary op over call results (f() + g()) fell through the LParen path - loop bodies parsed as statements (previously never parsed their body) - validator now validates loop bodies Tests: repair the broken-at-HEAD suite (38 compile errors), stale func keyword test, duplicate-func expectation -> all 18 test suites green Tooling/docs: - CI: install fasm, checkout submodules - gitignore: *.ssa, *.asm, /sdk/ fix (azlang-docs/src/sdk/ was ignored), book output; git rm tracked compiler binaries and .s/.ssa artifacts - docs: qbe.md -> fasm.md, assembly_linking.md -> direct_assembly.md, real measured sizes (198/228 byte hello world), ./sdk/src/*.s @link --- .github/workflows/tests.yml | 5 + .gitignore | 5 +- azlang-docs/src/SUMMARY.md | 4 +- .../src/getting-started/hello_world.md | 40 +- .../src/getting-started/installation.md | 14 +- azlang-docs/src/internals/assembly_linking.md | 28 - azlang-docs/src/internals/direct_assembly.md | 36 ++ azlang-docs/src/internals/fasm.md | 58 ++ azlang-docs/src/internals/qbe.md | 31 - azlang-docs/src/internals/validator.md | 2 +- azlang-docs/src/introduction.md | 27 +- azlang-docs/src/philosophy/doctrine.md | 8 +- azlang-docs/src/sdk/link.md | 45 ++ azlang-docs/src/sdk/standard_library.md | 65 ++ azlang-docs/src/sdk/syscalls.md | 46 ++ azlang-docs/src/syntax/enums.md | 27 +- azlang-docs/src/syntax/functions.md | 10 +- azlang-docs/src/syntax/modules.md | 6 +- azlang-docs/src/syntax/types.md | 8 +- compiler/cli | Bin 8888 -> 0 bytes compiler/cli.s | 25 - compiler/main.s | 25 - compiler/main.ssa | 10 - compiler/read | Bin 9040 -> 0 bytes compiler/read.s | 59 -- compiler/src/errors.rs | 6 +- compiler/src/lib.rs | 108 +++- compiler/src/pipline.rs | 61 +- compiler/src/tests/compiler_test.rs | 29 +- crates/parser/src/ast.rs | 2 +- crates/parser/src/binary_op.rs | 4 +- crates/parser/src/loop.rs | 36 +- crates/parser/src/tests/loop.rs | 5 +- crates/tokenizer/src/lib.rs | 21 +- crates/tokenizer/src/tests.rs | 4 +- crates/transpiler/src/lib.rs | 566 ++++++++++++------ crates/validator/src/lib.rs | 4 +- crates/validator/src/tests/assignment.rs | 8 +- crates/validator/src/tests/binary_op.rs | 26 +- crates/validator/src/tests/function_call.rs | 24 +- crates/validator/src/tests/function_def.rs | 21 +- crates/validator/src/tests/variable_decl.rs | 18 +- crates/validator/src/tests/while_loop.rs | 6 +- crates/validator/src/validate.rs | 10 +- readme.md | 9 +- sdk | 2 +- 46 files changed, 897 insertions(+), 657 deletions(-) delete mode 100644 azlang-docs/src/internals/assembly_linking.md create mode 100644 azlang-docs/src/internals/direct_assembly.md create mode 100644 azlang-docs/src/internals/fasm.md delete mode 100644 azlang-docs/src/internals/qbe.md create mode 100644 azlang-docs/src/sdk/link.md create mode 100644 azlang-docs/src/sdk/standard_library.md create mode 100644 azlang-docs/src/sdk/syscalls.md delete mode 100755 compiler/cli delete mode 100644 compiler/cli.s delete mode 100644 compiler/main.s delete mode 100644 compiler/main.ssa delete mode 100755 compiler/read delete mode 100644 compiler/read.s 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 b2404505968932c4fe69f8ef95c49e2629e9859f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8888 zcmeI2u}T9$5QgWXiLuHF76F@!1kxQ5Em8!zM!iC8RIocPCPYDV;cf-fXkjIJ5g#Mi z|arI>}=nfZ6-cJn38X7};n_^@DEz^wrLkVdwbN~Q;B)i?vd zgBBEZU(!A1u6_?`&!foL3wkwMu+1>aMRX|!nvdT;-S(#w+-CdFZ~pWZH@}slmf9%o%qr=weJR#1ix2`rKnMr{As_^VfDjM@LO=)z z0U;m+{zm|}_l|yWSATSJuFvzW32OKU0JcIdHtO%*oXdaC@*``hU_pb=_bFjhTN`J| z>6KJ305m+fPOoVP*=kaz zW%n6OnB9M%VZP5mVLEqK%c8bT#{@5=;csC@KU`lWeVud)GxP2kw~;Bv$*+Gy%lO`W Nn>j|=%MIk~zW^*`Mmwja94l8T_e$3D?WLV)M!uo->C`9xoaG>DcwuG`|iJ~t%&)GN_4T(23z zQWrf{NvqNt4KnW;;e@EcJxbuTW`O2E9DN6Ilf%$aa#*bh1gdHEC zJAil}ApOt1(SqC)NW3TUK`J4yU--6xrMv@(z56l#wH805c*3s_YVp@T{&e1BA8$VS zJtr~0H%Q6a5NnF@u*BqIYUahvVDw=!F&2|C<_kM3rny;OI2daeHkf%nD$Bfdg>}fy zxNrrj`4||MA7`!@k#;ud_hLD-bQt>FAysco|F-o0Ei>zxuxb^(UZ$AKDFWwVUFYBMg`W@OTn^Ldmxoasty{hK~622(frZ4P2 nAnYsP8O3C7GArrvA0v4eRMp1eDxdf5@9Iaf{$-UJjC%b&f{0$; 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