diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b684d00 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 NEScript Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c21c96 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# NEScript + +A statically-typed, compiled programming language for NES game development. + +NEScript compiles `.ne` source files directly into playable iNES ROM files, with no external assembler or linker dependencies. The compiler handles everything from source text to a ROM you can run in any NES emulator. + +## Quick Start + +```bash +# Build the compiler +cargo build --release + +# Compile an example +cargo run -- build examples/hello_sprite.ne + +# Run the output ROM in an emulator +# (produces examples/hello_sprite.nes) +``` + +## Hello World + +``` +game "Hello" { + mapper: NROM +} + +var px: u8 = 128 +var py: u8 = 120 + +on frame { + if button.right { px += 2 } + if button.left { px -= 2 } + if button.down { py += 2 } + if button.up { py -= 2 } + + draw Smiley at: (px, py) +} + +start Main +``` + +## Features + +- **Game-aware syntax** -- states, sprites, palettes, and input are first-class constructs +- **Full type system** -- `u8`, `i8`, `u16`, `bool`, fixed-size arrays (`u8[N]`) +- **Functions** -- with parameters, return types, `inline` hint, recursion detection +- **State machines** -- `state` with `on enter`, `on exit`, `on frame` handlers +- **Compile-time safety** -- call depth limits, recursion detection, type checking +- **Optimizer** -- constant folding, dead code elimination, strength reduction +- **Multiple mappers** -- NROM, MMC1, UxROM, MMC3 +- **Asset pipeline** -- PNG-to-CHR conversion, palette definitions, inline tile data +- **Debug support** -- `--debug` flag, source maps, Mesen-compatible symbol export +- **Single binary** -- no dependencies on ca65, Python, or any external tools + +## Documentation + +- **[Language Guide](docs/language-guide.md)** -- complete reference for every language feature +- **[Architecture](docs/architecture.md)** -- compiler internals and module overview +- **[NES Reference](docs/nes-reference.md)** -- hardware quick reference for contributors +- **[Examples README](examples/README.md)** -- how to build and run examples + +## Examples + +| Example | Features demonstrated | +|---------|---------------------| +| [`hello_sprite.ne`](examples/hello_sprite.ne) | D-pad input, sprite drawing | +| [`bouncing_ball.ne`](examples/bouncing_ball.ne) | Automatic movement, edge detection | +| [`coin_cavern.ne`](examples/coin_cavern.ne) | Multi-state game, functions, constants, gravity | +| [`arrays_and_functions.ne`](examples/arrays_and_functions.ne) | Arrays, functions, while loops, inline functions | +| [`state_machine.ne`](examples/state_machine.ne) | State transitions, on enter/exit, timers | +| [`sprites_and_palettes.ne`](examples/sprites_and_palettes.ne) | Inline CHR data, palettes, scroll, type casting | +| [`mmc1_banked.ne`](examples/mmc1_banked.ne) | MMC1 mapper, bank declarations, multiply | + +## Compiler Commands + +```bash +# Compile to ROM +nescript build game.ne + +# Compile with custom output path +nescript build game.ne --output my_game.nes + +# Type-check only (no ROM output) +nescript check game.ne + +# View generated 6502 assembly +nescript build game.ne --asm-dump + +# Enable debug mode +nescript build game.ne --debug +``` + +## Emulator Compatibility + +Output ROMs are standard iNES format and work with any NES emulator: + +- **[Mesen](https://www.mesen.ca/)** -- recommended, best debugging support +- **[FCEUX](https://fceux.com/)** +- **[Nestopia](http://nestopia.sourceforge.net/)** + +## Project Status + +NEScript implements all five planned milestones: + +| Milestone | Status | Key Features | +|-----------|--------|-------------| +| M1: Hello Sprite | Done | Full compiler pipeline, assembler, ROM builder | +| M2: Game Loop | Done | Functions, arrays, IR, optimizer, call graph analysis | +| M3: Asset Pipeline | Done | PNG-to-CHR, sprites, palettes, debug symbols | +| M4: Optimization | Done | Strength reduction, ZP promotion, type casting, asm-dump | +| M5: Bank Switching | Done | MMC1/UxROM/MMC3, bank declarations, software mul/div | + +210 tests across 14 modules, with CI running fmt, clippy, test, and example compilation on every push. + +## License + +[MIT](LICENSE) diff --git a/examples/README.md b/examples/README.md index 6663d9e..aab1bed 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,52 +2,32 @@ ## Quick Start -### 1. Build the compiler - -``` +```bash +# Build the compiler cargo build --release -``` -### 2. Compile an example +# Compile all examples +for f in examples/*.ne; do cargo run -- build "$f"; done -``` +# Or compile one cargo run -- build examples/hello_sprite.ne ``` -This produces `examples/hello_sprite.nes` — a valid iNES ROM file. - -### 3. Run in an emulator - -Open the `.nes` file in any NES emulator: - -- **[Mesen](https://www.mesen.ca/)** (recommended — best debugging support) -- **[FCEUX](https://fceux.com/)** -- **[Nestopia](http://nestopia.sourceforge.net/)** - -Use the d-pad (arrow keys in most emulators) to move the sprite. +Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX](https://fceux.com/), etc.) ## Examples -| File | Description | -|------|-------------| -| `hello_sprite.ne` | Move a smiley face with the d-pad | -| `bouncing_ball.ne` | A sprite that bounces around the screen automatically | +| File | Features | Description | +|------|----------|-------------| +| `hello_sprite.ne` | input, draw | Move a sprite with the d-pad | +| `bouncing_ball.ne` | if/else, variables | Auto-bouncing sprite with edge detection | +| `coin_cavern.ne` | states, functions, constants | 3-state game with gravity and coin collection | +| `arrays_and_functions.ne` | arrays, functions, while | Enemy array with collision detection | +| `state_machine.ne` | on enter/exit, transitions | Multi-state flow with timers | +| `sprites_and_palettes.ne` | sprites, palettes, scroll, cast | Inline CHR data, palette switching, type casting | +| `mmc1_banked.ne` | MMC1, banks, multiply | Banked mapper with software multiply | -## What you'll see - -The ROM displays a small 8x8 smiley-face sprite. This is a default tile built -into the compiler's CHR data. In `hello_sprite`, you control it with the d-pad. -In `bouncing_ball`, it moves on its own and bounces off the screen edges. - -### About sprite names - -In Milestone 1, the name in `draw Smiley at: (x, y)` is parsed but not -resolved to a specific tile — all draws use CHR tile 0 (the built-in smiley). -The `draw` syntax is forward-compatible: when `sprite` declarations and the -asset pipeline arrive in M3, names like `Smiley` will reference actual -sprite definitions with custom tile data from PNGs. - -## Emulator controls +## Emulator Controls | NES Button | Typical Key | |------------|-------------| @@ -57,17 +37,36 @@ sprite definitions with custom tile data from PNGs. | Start | Enter | | Select | Right Shift | -(Exact mappings vary by emulator — check your emulator's input settings.) +## About Sprites -## Compiler commands +Sprite names in `draw Player at: (x, y)` are parsed and recorded in the AST. +You can define sprites with inline CHR tile data: ``` +sprite Player { + chr: [0x3C, 0x42, 0x81, 0x81, 0x81, 0x81, 0x42, 0x3C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] +} +``` + +If no matching sprite declaration exists, the draw uses the built-in default +tile (a smiley face). See `sprites_and_palettes.ne` for a full example. + +## Compiler Commands + +```bash # Compile to ROM -cargo run -- build examples/hello_sprite.ne +cargo run -- build game.ne -# Compile with custom output path -cargo run -- build examples/hello_sprite.ne --output my_game.nes +# Custom output path +cargo run -- build game.ne --output my_game.nes -# Type-check only (no ROM output) -cargo run -- check examples/hello_sprite.ne +# Type-check only +cargo run -- check game.ne + +# View generated 6502 assembly +cargo run -- build game.ne --asm-dump + +# Debug mode +cargo run -- build game.ne --debug ``` diff --git a/examples/arrays_and_functions.ne b/examples/arrays_and_functions.ne new file mode 100644 index 0000000..bd2163d --- /dev/null +++ b/examples/arrays_and_functions.ne @@ -0,0 +1,83 @@ +// Arrays and Functions — demonstrates M2 features. +// +// Shows: arrays, functions with parameters and return values, +// while loops, constants, inline functions. +// +// Build: cargo run -- build examples/arrays_and_functions.ne + +game "Arrays Demo" { + mapper: NROM +} + +const NUM_ENEMIES: u8 = 4 +const SPEED: u8 = 1 + +var player_x: u8 = 128 +var player_y: u8 = 200 + +// Enemy positions stored in arrays +var enemy_x: u8[4] = [30, 80, 130, 200] +var enemy_y: u8[4] = [40, 80, 120, 60] + +// Function: clamp value to screen bounds +fun clamp(val: u8, max: u8) -> u8 { + if val > max { + return max + } + return val +} + +// Inline function: absolute difference +inline fun abs_diff(a: u8, b: u8) -> u8 { + if a > b { + return a - b + } + return b - a +} + +// Function: check collision between two points +fun check_collision(x1: u8, y1: u8, x2: u8, y2: u8) -> u8 { + var dx: u8 = abs_diff(x1, x2) + var dy: u8 = abs_diff(y1, y2) + if dx < 8 { + if dy < 8 { + return 1 + } + } + return 0 +} + +on frame { + // Player movement + if button.right { player_x += SPEED } + if button.left { player_x -= SPEED } + if button.down { player_y += SPEED } + if button.up { player_y -= SPEED } + + player_x = clamp(player_x, 240) + player_y = clamp(player_y, 224) + + // Update and draw enemies + var i: u8 = 0 + while i < NUM_ENEMIES { + // Move enemies down slowly + enemy_y[i] += 1 + if enemy_y[i] > 224 { + enemy_y[i] = 0 + } + + // Check collision with player + var hit: u8 = check_collision(player_x, player_y, enemy_x[i], enemy_y[i]) + if hit == 1 { + // Reset enemy position on collision + enemy_y[i] = 0 + } + + draw Enemy at: (enemy_x[i], enemy_y[i]) + i += 1 + } + + draw Player at: (player_x, player_y) +} + +start Main diff --git a/examples/mmc1_banked.ne b/examples/mmc1_banked.ne new file mode 100644 index 0000000..a06517b --- /dev/null +++ b/examples/mmc1_banked.ne @@ -0,0 +1,112 @@ +// MMC1 Banked — demonstrates M5 mapper and bank features. +// +// Shows: MMC1 mapper selection, bank declarations, +// vertical mirroring, software multiply. +// +// Build: cargo run -- build examples/mmc1_banked.ne + +game "Banked Game" { + mapper: MMC1 + mirroring: vertical +} + +// Declare PRG banks for organizing code/data +bank Level1Data: prg +bank Level2Data: prg +bank TileBank: chr + +const GRAVITY: u8 = 1 +const JUMP_VEL: u8 = 5 +const GROUND_Y: u8 = 200 + +var px: u8 = 64 +var py: u8 = 200 +var vy: u8 = 0 +var airborne: u8 = 0 +var level: u8 = 1 + +// Multiplication via the language (uses software multiply runtime) +fun scale_speed(base: u8, factor: u8) -> u8 { + return base * factor +} + +fun apply_gravity() { + if airborne == 1 { + vy += GRAVITY + py += vy + if py >= GROUND_Y { + py = GROUND_Y + airborne = 0 + vy = 0 + } + } +} + +state Level1 { + on enter { + px = 64 + py = GROUND_Y + level = 1 + } + + on frame { + // Horizontal movement with scaled speed + if button.right { + var spd: u8 = scale_speed(2, level) + px += spd + } + if button.left { + if px > 2 { + px -= 2 + } + } + + // Jump + if button.a { + if airborne == 0 { + airborne = 1 + vy = JUMP_VEL + } + } + + apply_gravity() + + // Advance to level 2 + if px > 230 { + transition Level2 + } + + draw Player at: (px, py) + } +} + +state Level2 { + on enter { + px = 10 + py = GROUND_Y + level = 2 + } + + on frame { + if button.right { px += 3 } + if button.left { px -= 3 } + + if button.a { + if airborne == 0 { + airborne = 1 + vy = JUMP_VEL + } + } + + apply_gravity() + + // Loop back + if button.select { + transition Level1 + } + + draw Player at: (px, py) + } +} + +start Level1 diff --git a/examples/sprites_and_palettes.ne b/examples/sprites_and_palettes.ne new file mode 100644 index 0000000..dc3349c --- /dev/null +++ b/examples/sprites_and_palettes.ne @@ -0,0 +1,69 @@ +// Sprites and Palettes — demonstrates M3 asset features. +// +// Shows: sprite declarations with inline CHR data, +// palette declarations, set_palette, type casting, scroll. +// +// Build: cargo run -- build examples/sprites_and_palettes.ne + +game "Asset Demo" { + mapper: NROM +} + +// Define a sprite with inline CHR tile data (16 bytes = one 8x8 tile) +// This is a simple arrow pointing right +sprite Arrow { + chr: [0x18, 0x1C, 0xFE, 0xFF, 0xFF, 0xFE, 0x1C, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] +} + +// Define a sprite with a heart shape +sprite Heart { + chr: [0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00, + 0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00] +} + +// Custom color palette +palette Warm { + colors: [0x0F, 0x06, 0x16, 0x26] +} + +palette Cool { + colors: [0x0F, 0x01, 0x11, 0x21] +} + +var px: u8 = 128 +var py: u8 = 120 +var scroll_x: u8 = 0 +var use_warm: u8 = 1 + +on frame { + // Movement + if button.right { px += 2 } + if button.left { px -= 2 } + if button.down { py += 2 } + if button.up { py -= 2 } + + // Scroll background + scroll_x += 1 + scroll(scroll_x, 0) + + // Toggle palette with A button + if button.a { + if use_warm == 1 { + set_palette Cool + use_warm = 0 + } else { + set_palette Warm + use_warm = 1 + } + } + + // Type cast demo + var wide: u16 = px as u16 + + // Draw sprites + draw Arrow at: (px, py) + draw Heart at: (px, py - 16) +} + +start Main diff --git a/examples/state_machine.ne b/examples/state_machine.ne new file mode 100644 index 0000000..d8dc966 --- /dev/null +++ b/examples/state_machine.ne @@ -0,0 +1,108 @@ +// State Machine — demonstrates multi-state game flow. +// +// Shows: multiple states with on enter/exit/frame handlers, +// transitions, state-local variables, button input. +// +// Build: cargo run -- build examples/state_machine.ne + +game "State Demo" { + mapper: NROM +} + +var global_score: u8 = 0 + +// ── Title Screen ── +state Title { + var blink_timer: u8 = 0 + + on enter { + blink_timer = 0 + } + + on frame { + blink_timer += 1 + + // Draw title logo + draw Logo at: (100, 80) + + // Blink "press start" indicator + if blink_timer < 30 { + draw Arrow at: (100, 140) + } + if blink_timer > 60 { + blink_timer = 0 + } + + if button.start { + transition Playing + } + } +} + +// ── Gameplay ── +state Playing { + var px: u8 = 128 + var py: u8 = 200 + var timer: u8 = 0 + + on enter { + px = 128 + py = 200 + timer = 0 + } + + on frame { + // Movement + if button.right { px += 2 } + if button.left { px -= 2 } + if button.up { py -= 2 } + if button.down { py += 2 } + + // Timer counts up + timer += 1 + + // After 180 frames (~3 seconds), end the level + if timer > 180 { + global_score += 10 + transition Victory + } + + // Press select to quit + if button.select { + transition Title + } + + draw Player at: (px, py) + } + + on exit { + // Could save state here + wait_frame + } +} + +// ── Victory Screen ── +state Victory { + var celebrate_timer: u8 = 0 + + on enter { + celebrate_timer = 0 + } + + on frame { + celebrate_timer += 1 + + draw Trophy at: (120, 100) + + // Return to title after a delay + if celebrate_timer > 120 { + transition Title + } + + if button.start { + transition Playing + } + } +} + +start Title diff --git a/src/parser/mod.rs b/src/parser/mod.rs index fa12ac9..be7509d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -39,6 +39,10 @@ impl Parser { .map_or(&TokenKind::Eof, |t| &t.kind) } + fn peek_at_offset(&self, offset: usize) -> Option<&TokenKind> { + self.tokens.get(self.pos + offset).map(|t| &t.kind) + } + fn current_span(&self) -> Span { self.tokens.get(self.pos).map_or(Span::dummy(), |t| t.span) } @@ -841,7 +845,10 @@ impl Parser { let mut frame = None; // Parse keyword arguments: at: (x, y), frame: n - while let TokenKind::Ident(_) = self.peek() { + // Only consume an ident if it's followed by ':', indicating a keyword arg. + while matches!(self.peek(), TokenKind::Ident(_)) + && self.peek_at_offset(1) == Some(&TokenKind::Colon) + { let (key, _) = self.expect_ident()?; self.expect(&TokenKind::Colon)?; match key.as_str() {