Небольшой интерпретируемый язык программирования, написанный на Rust ради BLAZING перформанса.
Цель проекта — пощупать, как реально работают лексер, парсер, AST, интерпретатор, области видимости и простая стандартная библиотека.
Rusthon — минималистичный, но уже довольно «живой» язык:
- статическая типизация (
int,bool,str,list); - переменные и присваивания;
if / elif / else;- циклы
whileи два вариантаfor; - пользовательские функции
func; - стандартные функции:
print,len,range; - списки и
for … inпо спискам, строкам и диапазонам; - лексические области видимости (стек окружений) и вызовы функций.
- Возможности языка
- Пример программы
- Сборка и запуск
- Архитектура проекта
- Язык Rusthon формально
- Как расширять язык
- Планы и TODO
- Лицензия
Поддерживаются базовые типы:
int— целое число (i64);bool— логический тип:true/false;str— строка;list— список значений языка (listпока гомогенность не проверяет строго, но хранитVec<Value>).
Внутренний тип интерпретатора:
pub enum Value {
Int(i64),
Bool(bool),
Str(String),
List(Vec<Value>),
Unit, // "ничего", используется как тип результата у print/return без значения и т.п.
}Объявление переменных — через var с явным типом:
var x: int = 10
var msg: str = "hello"
var ok: bool = true
var xs: list = [1, 2, 3]
Присваивание — просто =:
x = x + 1
msg = "new message"
Тип проверяется при инициализации (VarDecl). Дальше типы не меняются.
Поддерживаются:
- арифметика:
+,-,*,/; - сравнения:
==,!=,<,<=,>,>=.
Примеры:
var a: int = 2 + 3 * 4
var b: bool = a > 5
var s: str = "hello " + "world"
Классический if / elif / else с круглой скобкой вокруг условия и {} для блока:
if (x < 0) {
print("x < 0")
} elif (x == 0) {
print("x == 0")
} else {
print("x > 0")
}
В AST это:
Branch {
cond: Expr,
then_branch: Vec<Stmt>,
else_if_branches: Vec<Stmt>, // внутри Stmt::ElseIfBranch
else_branch: Vec<Stmt>,
}var i: int = 0
while (i < 3) {
print(i)
i = i + 1
}
В интерпретаторе условие должно давать bool, иначе — panic.
Если после for сразу идёт идентификатор и in, это foreach-форма:
// 1) for i in N: i = 0..N-1
for i in 5 {
print(i)
}
// 2) for ch in "hi!"
for ch in "hi!" {
print(ch)
}
// 3) for v in список
var xs: list = [10, 20, 30]
for v in xs {
print(v)
}
Интерпретация:
for i in 5—iпробегает от0до4;for ch in "hi!"—ch— строка длиной 1 (символ);for v in xs—v— элементы списка.
Второй вариант — псевдо-C-стиль, но в упрощённом виде: for (expr) { ... }.
Сейчас он реализован как «цикл с условием», то есть фактически аналог while с синтаксисом:
for (x < 10) {
print(x)
x = x + 1
}
В AST:
For {
cond: Expr,
body: Vec<Stmt>,
}(инициализация и шаг пока не вынесены явно в грамматику, это можно добавить позже.)
Определение функции:
func add(a: int, b: int) {
return a + b
}
func fact(n: int) {
var res: int = 1
var i: int = 1
while (i <= n) {
res = res * i
i = i + 1
}
return res
}
Возврат значения — через return.
return может быть без аргумента — тогда возвращается Unit.
Функции хранятся в AST как:
pub struct Function {
pub name: String,
pub params: Vec<(String, Type)>,
pub body: Vec<Stmt>,
}И в Program:
pub struct Program {
pub functions: Vec<Function>,
pub stmts: Vec<Stmt>, // глобальные операторы
}Вызов функции:
var s: int = add(2, 3)
print(s)
При вызове:
- создаётся новый
scope(новыйHashMapвenv_stack); - параметры кладутся как локальные переменные;
- выполняется тело; при
returnзначение пробрасывается наружу; - локальная область видимости удаляется.
Литералы списков:
var xs: list = [1, 2, 3]
var strs: list = ["hello", "world"]
Внутри интерпретатора:
Expr::ListLiteral(Vec<Expr>) → Value::List(Vec<Value>)Списки используются, в частности, для for v in xs и в функции len(xs).
Реализована в stdlib.rs через функцию:
pub fn call_builtin(name: &str, args: &Vec<Value>) -> Option<Value>Сейчас есть:
Выводит все аргументы через пробел и добавляет перевод строки:
print("answer =", 42)
Поддерживает int, bool, str, list, Unit.
Возвращает длину строки или списка:
var s: str = "hello"
var xs: list = [1, 2, 3]
print(len(s)) # 5
print(len(xs)) # 3
Создаёт список целых чисел:
range(n) # [0, 1, 2, ..., n-1]
range(a, b) # [a, a+1, ..., b-1]
for i in range(5) {
print(i)
}
Фрагмент демонстрации возможностей языка:
func add(a: int, b: int) {
return a + b
}
func fact(n: int) {
var res: int = 1
var i: int = 1
while (i <= n) {
res = res * i
i = i + 1
}
return res
}
func test_if(x: int) {
if (x < 0) {
print("x < 0")
} elif (x == 0) {
print("x == 0")
} else {
print("x > 0")
}
}
func main_logic() {
print("== функции add и fact ==")
var s: int = add(2, 3)
print("add(2, 3) =")
print(s)
var f: int = fact(5)
print("fact(5) =")
print(f)
print("== if / elif / else ==")
test_if(-1)
test_if(0)
test_if(1)
print("== foreach по range и list ==")
for i in range(5) {
print(i)
}
var xs: list = [10, 20, 30]
for v in xs {
print(v)
}
}
var answer: int = 42
print("== глобальные переменные и main_logic ==")
print(answer)
main_logic()
- Установленный Rust toolchain (стабильная версия);
cargoв$PATH.
Проверить:
rustc --version
cargo --versionКлонируем репозиторий и собираем:
git clone https://github.com/<USER>/<REPO>.git
cd <REPO>
# Debug-сборка
cargo build
# Release-сборка
cargo build --releaseПосле cargo build --release бинарник появится в:
target/release/Rusthon
Rusthon принимает путь к .rht-файлу первым аргументом:
./target/release/Rusthon path/to/program.rhtЕсли файла нет или расширение не .rht, интерпретатор завершится с ошибкой.
Пример:
./target/release/Rusthon examples/demo.rhtСтруктура модулей примерно такая:
src/
ast.rs // описание AST: Expr, Stmt, Function, Program, Type, BinOp
lexer.rs // лексер: разбор текста в токены
parser.rs // парсер: токены -> AST
interpreter.rs // интерпретатор: выполнение AST
stdlib.rs // встроенные функции (print, len, range, ...)
main.rs // точка входа: связывает всё вместе
Отвечает за разбор сырого текста в токены (Token):
-
пропускает пробелы и табы;
-
определяет:
Ident(String),IntLiteral(i64),StrLiteral(String);- ключевые слова:
var,func,if,elif,else,while,for,in,true,false,returnи т.д. - операторы и разделители:
+,-,*,/,%,==,!=,<,<=,>,>=,(,),{,},[,],:,,.
Используется парсером как итератор:
let lexer = Lexer::new(&program_text);
let mut parser = Parser::new(lexer);AST (Abstract Syntax Tree) — это абстрактное синтаксическое дерево, структура данных, которая описывает программу не в виде строки, а в виде иерархии узлов: выражения, операторы, функции и т.п.
Примеры:
Expr::Binary— бинарное выражение (a + b,x < y, ...);Stmt::VarDecl— объявление переменной;Stmt::While— циклwhile;Stmt::ForEach—for v in xs { ... };Function— пользовательская функция;Program— корень дерева (список функций + глобальных операторов).
Интерпретатор ходит по этому дереву и выполняет программу.
Парсер преобразует поток Token в AST:
-
реализует рекурсивный спуск;
-
учитывает приоритет операторов:
parse_primary→ числа, строки, идентификаторы,(...), списки[...];parse_factor→ умножение/деление и вызовыfunc(...);parse_term→*и/;parse_expr→+,-, сравнения==,!=,<,>, ...
-
парсит:
- объявления переменных:
var name: type = expr; - присваивания:
name = expr; if / elif / else;whileиfor;func name(params) { body };return.
- объявления переменных:
В случае ошибки парсер вызывает error(...), выводит сообщение с текущим токеном и завершает процесс.
Исполняет AST:
-
хранит стек окружений:
struct Interpreter { env_stack: Vec<HashMap<String, Value>>, functions: HashMap<String, Function>, }
-
при старте:
- загружает все
Functionвfunctions; - выполняет глобальные операторы (
program.stmts).
- загружает все
-
области видимости:
-
push_env()/pop_env(); -
новый scope создаётся:
- при входе в функцию;
- при выполнении блока (
exec_block) for/while/if-ветки.
-
-
переменные:
define_var(name, value)— кладёт в текущий (верхний) scope;assign_var— ищет переменную снизу вверх по стеку и обновляет значение;get_var— ищет переменную при чтении.
-
выражения:
eval_expr(&Expr) -> Value;- арифметика и сравнения в
eval_bin.
-
операторы:
-
exec_stmt(&Stmt) -> Option<Value>:None— обычное выполнение;Some(value)— проброшенныйreturnиз функции.
-
Содержит реализацию встроенных функций окружения:
print(...)len(x)range(...)
Интерпретатор сначала пробует вызвать builtin:
if let Some(result) = stdlib::call_builtin(&callee, &value_args) {
return result;
}А если такое имя не найдено, пытается вызвать пользовательскую функцию.
Связывает всё вместе:
use std::env;
use std::fs;
mod ast;
mod interpreter;
mod lexer;
mod parser;
mod stdlib;
use interpreter::Interpreter;
use lexer::Lexer;
use parser::Parser;
fn main() {
// Находим первый аргумент, который заканчивается на ".rht"
let args: Vec<String> = env::args().collect();
let path = args
.iter()
.find(|arg| arg.ends_with(".rht"))
.expect("❌ You must pass a .rht program file as an argument.")
.clone();
// Читаем файл программы
let program_text = fs::read_to_string(&path)
.expect("❌ Failed to read the program file.");
// Лексер + парсер → AST
let lexer = Lexer::new(&program_text);
let mut parser = Parser::new(lexer);
let program = parser.parse_program();
// Интерпретатор
let mut interp = Interpreter::new();
interp.run(&program);
}Это не строгий EBNF, но даёт общее ощущение грамматики:
program ::= (function | stmt)* EOF
function ::= "func" IDENT "(" param_list? ")" block
param_list ::= param ("," param)*
param ::= IDENT ":" type
type ::= "int" | "bool" | "str" | "list"
stmt ::= var_decl
| assign
| if_stmt
| while_stmt
| for_stmt
| return_stmt
| expr_stmt
var_decl ::= "var" IDENT ":" type "=" expr NEWLINE?
assign ::= IDENT "=" expr NEWLINE?
if_stmt ::= "if" "(" expr ")" block
("elif" "(" expr ")" block)*
("else" block)?
while_stmt ::= "while" "(" expr ")" block
for_stmt ::= "for" "(" expr ")" block
| "for" IDENT "in" expr block
return_stmt ::= "return" expr? NEWLINE?
expr_stmt ::= expr NEWLINE?
block ::= "{" NEWLINE* stmt* NEWLINE* "}"
expr ::= term (("+" | "-" | "==" | "!=" | "<" | "<=" | ">" | ">=") term)*
term ::= factor (("*" | "/") factor)*
factor ::= primary
| primary "(" arg_list? ")" // вызовы функций
primary ::= INT_LITERAL
| STR_LITERAL
| "true"
| "false"
| IDENT
| "(" expr ")"
| list_literal
list_literal ::= "[" (expr ("," expr)*)? "]"
arg_list ::= expr ("," expr)*
- Открыть
stdlib.rs. - Добавить новый кейс в
match name:
"upper" => {
if args.len() != 1 {
panic!("upper() expects exactly 1 argument");
}
let s = match &args[0] {
Value::Str(s) => s.clone(),
_ => panic!("upper() expects a string"),
};
Some(Value::Str(s.to_uppercase()))
}- Теперь в языке можно писать:
print(upper("hello"))
- Добавить вариант в
BinOp(вast.rs):
pub enum BinOp {
// ...
Mod, // %
}- В
lexer.rsубедиться, что%лексится как отдельный токен (Percentуже есть). - В
parser.rsдобавить разбор%вparse_term:
Token::Percent => {
self.bump();
let rhs = self.parse_factor();
node = Expr::Binary {
left: Box::new(node),
op: BinOp::Mod,
right: Box::new(rhs),
};
}- В
interpreter.rsдобавить обработку:
BinOp::Mod => match (left, right) {
(Value::Int(l), Value::Int(r)) => Value::Int(l % r),
_ => panic!("Type error in modulo"),
},Идеи для развития Rusthon:
- Логические операторы
&&,||, унарный!с приоритетами и short-circuit. - Унарный минус (
-x). - Более «настоящий» C-style
for (init; cond; step)с явными полями в AST. - Комментарии (
# ...или// ...) на уровне лексера. - Нормальная система ошибок (
Resultвместо тотальныхpanic!). - Типизация списков (
list[int],list[str]и т.п.). - Встроенный
main()по умолчанию (если функцияmainопределена — вызывать её автоматически). - Юнит-тесты (
cargo test) для лексера, парсера и интерпретатора. - CI (GitHub Actions) с автоматической сборкой и запуском тестов.
Лицензия указана в файле LICENSE.