A Rust-inspired scripting language for Godot 4.x
Quick Links: 📖 Docs | 🐛 Issues | 💬 Discussions | ❓ FAQ | 🔧 Troubleshooting | 📋 Error Codes
FerrisScript (named after Ferris 🦀, the Rust mascot) is a statically-typed, Rust-inspired scripting language designed specifically for Godot 4.x game development. It brings Rust's safety and performance philosophy to game scripting while maintaining a lightweight, easy-to-learn syntax.
- Familiar Syntax: If you know Rust, you already know 80% of FerrisScript
- Type Safety: Catch bugs at compile-time, not in production
- Performance: Static type checking enables optimization opportunities
- No GC Pauses: Deterministic performance for game loops
- Better Tooling: Static typing enables IDE features (autocomplete, go-to-definition)
- Easier Refactoring: Type checker catches breaking changes automatically
- Self-Documenting: Types serve as inline documentation
- Gradual Learning: Start simple, grow into advanced features
- Clear Contracts: Function signatures document expected inputs/outputs
- Fewer Runtime Errors: Many bugs caught before playtesting
- Code Confidence: Refactor fearlessly with type safety
- Performance Baseline: 16K+ function calls per frame at 60 FPS
TL;DR: FerrisScript brings Rust's "if it compiles, it probably works" philosophy to game scripting, making your game development faster and more reliable.
If your FerrisScript compiles, it won't crash Godot.
FerrisScript provides three layers of type safety that work together to catch bugs before they reach production:
The FerrisScript compiler catches errors before you ever run your game:
- ✅ Variable Type Mismatches: Can't assign
Stringtoi32variable - ✅ Function Signatures: Parameters and return types validated
- ✅ Property Hint Compatibility: Range hints only on numbers, enum hints only on strings
- ✅ Signal Parameters: Type-checked when declared and emitted
- ✅ Immutability Violations: Can't modify non-
mutvariables
Example: Type error caught at compile-time
// GDScript - runs until line 5, then crashes
var health = 100
health = "low" # Type changes at runtime
func take_damage(amount):
health -= amount # Runtime error: can't subtract from string!
// FerrisScript - compile error immediately
let mut health: i32 = 100;
health = "low"; // ❌ Compile error: expected i32, found String
// Error E201: Type mismatch at line 2, column 10Result: Your game never crashes from type errors. If it compiles, it works.
When scripts are loaded and attached to nodes:
- ✅ Property Metadata: Validated against Inspector expectations
- ✅ Signal Definitions: Registered with correct parameter types
- ✅ Lifecycle Functions: Type-checked (_ready, _process, etc.)
Even at runtime, FerrisScript prevents crashes:
- ✅ Variant Conversion: Safe defaults for type mismatches
- ✅ NaN/Infinity Handling: Prevents math crashes
- ✅ Property Bounds: Range clamping enforced
| Check Type | FerrisScript | GDScript | C# (Godot) |
|---|---|---|---|
| Variable types | ✅ Compile-time | ✅ Compile-time | |
| Function signatures | ✅ Compile-time | ✅ Compile-time | |
| Property hints | ✅ Compile-time | ✅ Compile-time | |
| Signal parameters | ✅ Compile-time | ✅ Compile-time | |
| Hot reload | ⏳ Planned (v0.1.0) | ✅ Yes | |
| Node property access | ⏳ Planned (v0.2.0) | ✅ Compile-time |
Legend: ✅ Full support |
| Feature | FerrisScript | GDScript |
|---|---|---|
| Type System | Static, compile-time checked | Dynamic with optional hints |
| Error Detection | Compile-time (before running game) | Runtime (during gameplay) |
| Performance | ~1 μs/function call | ~2-3 μs/function call* |
| IDE Support | LSP in development (v0.0.5+) | Excellent (built-in) |
| Learning Curve | Moderate (Rust-like syntax) | Easy (Python-like) |
| Refactoring Safety | High (type checker catches breaks) | Medium (manual testing needed) |
| Godot Integration | Via GDExtension | Native |
| Hot Reload | Yes | Yes |
| Maturity | Alpha (v0.0.4) | Production-ready |
* Performance comparison is preliminary and varies by use case. Detailed benchmarks are documented in version-specific documentation.
When to Choose FerrisScript:
- You prefer static typing and compile-time safety
- Coming from Rust/TypeScript/C# background
- Building complex systems that benefit from type checking
- Want performance predictability (no GC pauses)
- Need deterministic execution (multiplayer, replays)
When to Choose GDScript:
- Prototyping and rapid iteration
- Small to medium projects
- Prefer dynamic typing flexibility
- Want seamless Godot editor integration
- Learning game development for the first time
Use Both: FerrisScript and GDScript can coexist in the same project. Use FerrisScript for performance-critical systems and GDScript for rapid prototyping.
- 🦀 Rust-Inspired Syntax - Familiar to Rust developers, easy for beginners
- ⚡ Static Type Checking - Catch errors at compile-time (843 tests, 82% coverage)
- 🔒 Immutability by Default - Safe by default, explicit
mutfor mutations - 🎯 Zero-Cost Abstractions - Compiled to efficient runtime execution
- 📦 Minimal Dependencies - Lightweight and fast compilation
- 🎮 GDExtension Support - Native Godot 4.x integration via
gdext - 🎨 @export Annotations - Inspector integration with property hints (range, enum, file, multiline, color)
- 🔔 Signal System - Declare and emit custom signals visible in Inspector
- 📊 Godot Type Literals - Direct construction of
Vector2,Color,Rect2,Transform2D - 🌳 Node Query Functions -
get_node(),get_parent(),has_node(),find_child() - ⚡ Lifecycle Callbacks -
_ready(),_process(),_physics_process(),_input(),_unhandled_input()
- 🎨 VS Code Extension - Syntax highlighting, IntelliSense, code snippets, hover tooltips
- 🧪 Testing Infrastructure - 4-layer testing (unit, integration, GDExtension, benchmarks)
- 📝 Error Messages - Clear, actionable error messages with error codes
- 📖 Documentation - Comprehensive guides, examples, and API docs
FerrisScript has syntax highlighting and code snippets for Visual Studio Code:
- Syntax Highlighting: Keywords (
fn,let,mut,if,else,while,return), types (i32,f32,bool,String,Vector2,Node), operators, comments, strings - Code Snippets:
_ready,_process,let,fn,if,while, and more - Auto-closing: Brackets, quotes, comment toggling
- Language Configuration: Folding, indentation, word patterns
Installation: Copy extensions/vscode/ to your VS Code extensions folder:
# Windows
cp -r extensions/vscode ~/.vscode/extensions/ferrisscript-0.0.4
# Or use a symbolic link for development
mklink /D "%USERPROFILE%\.vscode\extensions\ferrisscript-0.0.4" "path\to\FerrisScript\extensions\vscode"Reload VS Code: Press Ctrl+Shift+P → "Developer: Reload Window"
- Code Completion (Ctrl+Space): Keywords, types, built-in functions with context awareness
- Hover Tooltips: Documentation and examples for keywords, types, and functions
- Problem Panel: Real-time compiler errors with inline diagnostics and error codes
- File Icons: Custom
.ferrisfile icons in Explorer
See extensions/vscode/README.md for full features, snippet reference, and known limitations.
Future: Full LSP with go-to-definition, find references, and rename coming in v0.0.5.
- Rust 1.70+ (Install Rust)
- Godot 4.2+ (Download Godot)
- Git (for cloning the repository)
# Clone the repository
git clone https://github.com/dev-parkins/FerrisScript.git
cd FerrisScript
# Build the project
cargo build --workspace
# Run tests
cargo test --workspace-
Build the GDExtension:
cargo build --package ferrisscript_godot_bind
Note for Godot 4.3+: The project is configured with
api-4-3feature for compatibility. If you encounter initialization errors, ensurecrates/godot_bind/Cargo.tomlhas the correct API version feature enabled. -
Open the test project:
- Open Godot 4.2+
- Import project from
godot_test/project.godot
-
Create your first script:
// my_script.ferris fn _ready() { print("Hello from FerrisScript!"); } fn _process(delta: f32) { self.position.x += 50.0 * delta; }
-
Attach to a node:
- Add
FerrisScriptNodeto your scene - Set
script_pathtores://scripts/my_script.ferris - Run your game!
- Add
// Variables - immutable by default
let name: String = "Ferris";
let age: i32 = 42;
// Mutable variables - explicit opt-in
let mut counter: i32 = 0;
counter = counter + 1;
// Functions
fn greet(name: String) -> String {
return "Hello, " + name;
}
// Control flow
if age > 18 {
print("Adult");
} else {
print("Minor");
}
// Loops
let mut i: i32 = 0;
while i < 10 {
print(i);
i = i + 1;
}// Global state persists between frames
let mut velocity: f32 = 0.0;
let gravity: f32 = 980.0;
fn _ready() {
print("Game started!");
}
fn _process(delta: f32) {
// Access node properties via 'self'
velocity = velocity + gravity * delta;
self.position.y += velocity * delta;
// Bounce at ground level
if self.position.y > 500.0 {
velocity = -velocity * 0.8;
self.position.y = 500.0;
}
}Use @export annotations to expose variables to Godot's Inspector:
// Basic exports
@export let speed: f32 = 100.0;
@export let jump_force: f32 = 500.0;
// Range hints (min, max) - clamps values in Inspector
@export(range, 0.0, 10.0) let health: f32 = 5.0;
// Enum hints - dropdown selector in Inspector
@export(enum, "Idle", "Walk", "Run") let state: String = "Idle";
// File hints - file picker in Inspector
@export(file, "*.png", "*.jpg") let texture_path: String = "";Inspector Features:
- Real-time Editing: Modify values in Inspector during gameplay
- Automatic Clamping: Range hints enforce min/max bounds
- Type Validation: Compile-time checks for correct hint usage
- Default Values: Inspector shows initial values from script
Declare and emit custom signals for communication between nodes:
// Declare signals at file scope
signal health_changed(new_health: f32);
signal player_died();
let mut health: f32 = 100.0;
fn take_damage(amount: f32) {
health = health - amount;
emit("health_changed", health); // Emit with parameter
if health <= 0.0 {
emit("player_died"); // Emit without parameters
}
}Signal Features:
- Type-Checked Parameters: Compile-time validation of signal signatures
- Godot Integration: Signals visible and connectable in Godot's Inspector
- Flexible Emission: Use
emit("signal_name", params...)in any function
FerrisScript supports the following types:
- Primitives:
i32,f32,bool,String - Godot Types:
Vector2,Color,Rect2,Transform2D,Node,Node2D - Type Inference: Literals are automatically typed
- Type Coercion:
i32→f32automatic conversion
Construct Godot types directly with field syntax:
// Vector2 - 2D position/velocity
let position = Vector2 { x: 100.0, y: 200.0 };
// Color - RGBA color values
let red = Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
// Rect2 - 2D rectangle (position + size)
let pos = Vector2 { x: 0.0, y: 0.0 };
let size = Vector2 { x: 100.0, y: 50.0 };
let rect = Rect2 { position: pos, size: size };
// Transform2D - 2D transformation (position + rotation + scale)
let p = Vector2 { x: 100.0, y: 200.0 };
let s = Vector2 { x: 2.0, y: 2.0 };
let transform = Transform2D {
position: p,
rotation: 1.57, // radians
scale: s
};Type Requirements:
Vector2: fieldsx,y(bothf32)Color: fieldsr,g,b,a(allf32, 0.0-1.0 range)Rect2: fieldsposition,size(bothVector2)Transform2D: fieldsposition,scale(Vector2),rotation(f32)
FerrisScript is designed for game scripting performance with predictable overhead:
| Operation | Performance | Notes |
|---|---|---|
| Lexer | 384 ns - 3.74 μs | Per-script compilation |
| Parser | 600 ns - 7.94 μs | Per-script compilation |
| Type Checker | 851 ns - 3.58 μs | Per-script compilation |
| Function Call | ~1.05 μs | Per-call overhead at runtime |
| Loop Iteration | ~180 ns | Per-iteration overhead |
Real-World Performance:
- 60 FPS Budget: 16.67 ms per frame
- Function Calls/Frame: ~16,000 calls possible at 60 FPS
- Compilation: Sub-millisecond for typical game scripts
- Memory: Minimal overhead (~1 KB per script)
Optimization Tips:
- Cache Frequently Used Values: Store
self.positionin local variables - Minimize Cross-Boundary Calls: Batch operations when possible
- Use Appropriate Types:
f32for game math,i32for counters - Profile First: Use Godot's profiler to identify bottlenecks
Note: Detailed performance analysis and benchmarking methodology are documented in version-specific documentation.
ferrisscript/
├── Cargo.toml # Workspace root
├── README.md # This file
├── crates/
│ ├── compiler/ # Lexer, Parser, Type Checker (543 tests)
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs # Public compile() API
│ │ ├── lexer.rs # Tokenization
│ │ ├── parser.rs # AST generation
│ │ ├── type_checker.rs# Static type checking
│ │ └── ast.rs # AST definitions
│ ├── runtime/ # Execution engine (110 tests)
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs # Runtime interpreter
│ ├── godot_bind/ # Godot 4.x integration (11 tests)
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs # GDExtension bindings
│ └── test_harness/ # Testing infrastructure (38 tests)
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # ferris-test CLI
│ └── lib.rs # Test runner, output parser
├── examples/ # 26 example scripts
│ ├── hello.ferris # Basic _ready callback
│ ├── move.ferris # Movement example
│ ├── signals.ferris # Signal system demo
│ ├── struct_literals_*.ferris # Godot type construction
│ └── node_query_*.ferris # Scene tree queries
├── godot_test/ # Godot test project
│ ├── project.godot
│ ├── ferrisscript.gdextension
│ └── scripts/ # 17 integration test scripts
│ ├── export_properties_test.ferris
│ ├── signal_test.ferris
│ └── process_test.ferris
├── extensions/ # Editor extensions
│ └── vscode/ # VS Code extension (v0.0.4)
│ ├── syntaxes/ # Syntax highlighting
│ ├── snippets/ # Code snippets
│ └── language-configuration.json
└── docs/ # Documentation
├── testing/ # Testing guides and matrices
│ ├── README.md # Testing hub
│ ├── TESTING_GUIDE.md # Comprehensive guide
│ └── TEST_MATRIX_*.md # Coverage tracking
├── planning/ # Version roadmaps
├── archive/ # Historical documentation
├── ARCHITECTURE.md # System design
├── DEVELOPMENT.md # Dev workflow
└── CONTRIBUTING.md # Contribution guide
Quick Links:
- Examples: examples/README.md - 26 annotated examples
- Testing: docs/testing/README.md - 4-layer testing strategy
- Architecture: docs/ARCHITECTURE.md - System design
- Development: docs/DEVELOPMENT.md - Dev workflow
- Contributing: CONTRIBUTING.md - Contribution guidelines
# Debug build (faster compilation)
cargo build --workspace
# Release build (optimized)
cargo build --workspace --release
# Run all tests
cargo test --workspace# Compiler only
cargo build --package ferrisscript_compiler
# Runtime only
cargo build --package ferrisscript_runtime
# Godot extension only
cargo build --package ferrisscript_godot_bind# All tests (843 tests)
cargo test --workspace
# Compiler tests (543 tests)
cargo test --package ferrisscript_compiler
# Runtime tests (110 tests)
cargo test --package ferrisscript_runtime
# Test harness tests (38 tests)
cargo test --package ferrisscript_test_harness
# Watch mode (with cargo-watch)
cargo watch -x "test --workspace"cargo build --package ferrisscript_godot_bindThis creates:
- Windows:
target/debug/ferrisscript_godot_bind.dll - Linux:
target/debug/libferrisscript_godot_bind.so - macOS:
target/debug/libferrisscript_godot_bind.dylib
- Create a new Godot 4.2+ project
- Create
.gdextensionfile in your project root:
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.2
[libraries]
windows.debug.x86_64 = "res://../target/debug/ferrisscript_godot_bind.dll"
windows.release.x86_64 = "res://../target/release/ferrisscript_godot_bind.dll"
linux.debug.x86_64 = "res://../target/debug/libferrisscript_godot_bind.so"
linux.release.x86_64 = "res://../target/release/libferrisscript_godot_bind.so"
macos.debug = "res://../target/debug/libferrisscript_godot_bind.dylib"
macos.release = "res://../target/release/libferrisscript_godot_bind.dylib"Create a .ferris file in your project:
// scripts/player.ferris
let mut speed: f32 = 200.0;
fn _ready() {
print("Player initialized!");
}
fn _process(delta: f32) {
// Your game logic here
self.position.x += speed * delta;
}- Add
FerrisScriptNode(extends Node2D) to your scene - In the Inspector, set
script_pathtores://scripts/player.ferris - Run your game!
FerrisScript comes with comprehensive examples to help you get started:
Difficulty: Beginner
Learn the basics of FerrisScript with a simple "Hello, World!" script.
- Using the
_ready()lifecycle hook - Calling builtin functions (
print) - Basic FerrisScript syntax
fn _ready() {
print("Hello from FerrisScript!");
}
Difficulty: Beginner
Create smooth movement with frame-by-frame updates.
- Using
_process(delta)for animations - Accessing node properties (
self.position) - Understanding delta time for framerate-independent movement
fn _process(delta: f32) {
self.position.x += 50.0 * delta;
}
Difficulty: Intermediate
Build a bouncing animation with boundary checks.
- Global variables and state management
- Conditional statements (
if) - Direction reversal and boundary detection
let mut dir: f32 = 1.0;
fn _process(delta: f32) {
self.position.x += dir * 100.0 * delta;
if self.position.x > 10.0 {
dir = -1.0;
}
if self.position.x < -10.0 {
dir = 1.0;
}
}
functions.ferris: Function definitions and callscollections.ferris: Arrays and dictionaries (v0.1.0+)match.ferris: Pattern matching (v0.1.0+)
See the examples/ directory for all available scripts.
print(value)- Print to Godot console
_ready()- Called when node enters the scene tree_process(delta: f32)- Called every frame
Access node properties via self:
self.position- Node's position (Vector2)self.position.x- X coordinate (f32)self.position.y- Y coordinate (f32)
FerrisScript uses a 4-layer testing strategy to ensure quality and reliability:
┌─────────────────────────────────────────────┐
│ Layer 4: Manual Testing (Godot Editor) │ ← Feature validation
├─────────────────────────────────────────────┤
│ Layer 3: Integration Tests (.ferris) │ ← End-to-end behavior
├─────────────────────────────────────────────┤
│ Layer 2: GDExtension Tests (GDScript) │ ← Godot bindings
├─────────────────────────────────────────────┤
│ Layer 1: Unit Tests (Rust) │ ← Pure logic
└─────────────────────────────────────────────┘
# Run all unit tests (843 tests)
cargo test --workspace
# Run specific test types
cargo test -p ferrisscript_compiler # Compiler tests (543 tests)
cargo test -p ferrisscript_runtime # Runtime tests (110 tests)
ferris-test --all # Integration tests (15+ scripts)
# Run with coverage
cargo llvm-cov --workspace --html # Generates HTML report| Test Type | Count | Coverage | Description |
|---|---|---|---|
| Compiler | 543 | ~85% | Lexer, parser, type checker |
| Runtime | 110 | ~80% | Interpreter, execution engine |
| GDExtension | 11 | ~70% | Godot bindings (10 ignored*) |
| Test Harness | 38 | ~90% | ferris-test CLI |
| Integration | 15+ | N/A | End-to-end .ferris scripts |
| Total | 843 | ~82% | Across all layers |
* Some tests require Godot runtime and are covered by integration tests
Run .ferris scripts headlessly against Godot:
# Run all integration tests
ferris-test --all
# Run specific test
ferris-test --script godot_test/scripts/signal_test.ferris
# Filter by name
ferris-test --all --filter "export"
# JSON output for CI
ferris-test --all --format json > results.jsonThe godot_test/ directory contains a complete test project:
# 1. Build extension
cargo build --package ferrisscript_godot_bind
# 2. Open in Godot
# Import godot_test/project.godot
# 3. Run tests (F5)
# Check Output panel for results- Testing Hub - Central testing documentation ⭐ START HERE
- Testing Guide - Complete patterns and procedures
- Test Matrices - Systematic coverage tracking
- Test Harness Architecture - ferris-test design
See docs/testing/README.md for comprehensive testing documentation.
Core Language (Phases 1-3):
- Lexer with full tokenization
- Parser with operator precedence and error recovery
- Type checker with static analysis (65+ error codes)
- Runtime interpreter with ~1 μs/function call
- Mutable variable tracking (immutable by default)
- Control flow (if/else, while loops)
- Function definitions and calls
- Global state persistence across frames
Godot Integration (Phase 4-5):
- Godot 4.x GDExtension integration via
gdext -
_ready(),_process(),_physics_process()callbacks - Self binding for node property access
- Signal system (declare & emit custom signals)
- Godot type literals (
Vector2,Color,Rect2,Transform2D) - @export annotations with property hints
- Inspector integration (real-time editing, clamping, validation)
- Node lifecycle functions (
_enter_tree(),_exit_tree(),_input()) - Node query functions (
get_node(),get_parent(),find_child(),has_node())
Quality & Testing:
- 843 tests passing (543 compiler + 110 runtime + 38 harness + 15 integration + 137 other)
- Comprehensive error messages with hints and suggestions
- VS Code extension with syntax highlighting, snippets, and IntelliSense
- Headless testing infrastructure
- Detailed documentation and examples
- Arrays and collections
- For loops
- String interpolation
- More Godot types (Node3D, Input, etc.)
- Struct definitions
- Match expressions
- LSP support for IDE integration (go-to-definition, find references, rename)
Contributions are welcome! Please feel free to submit issues or pull requests.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests (
cargo test --workspace) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
We follow Conventional Commits:
feat:- New featurefix:- Bug fixdocs:- Documentation changestest:- Test additions/changesrefactor:- Code refactoringchore:- Maintenance tasks
This project is licensed under the MIT License - see the LICENSE file for details.
- Ferris 🦀 - The Rust mascot that inspired our name
- Godot Engine - Amazing open-source game engine
- gdext - Rust bindings for Godot 4
- Rust Community - For the incredible language and ecosystem
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: docs/
Made with 🦀 and ❤️ for the Godot community