/Trust-Interpreter

Trust language interpreter

Primary LanguageC++GNU General Public License v3.0GPL-3.0

Trust Programming Language

Trust (trash + Rust) - the only programming language you can trust.

Описание

Trust - это интерпретируемый язык программирования, синтаксис которого заимствован из Rust. В нём есть:

  • базовые типы
  • условные конструкции
  • циклы for и while
  • функции

Типы

В Trust есть следующие типы:

  • bool - логический тип
  • i32 - целое 32 битное знаковое число
  • f64 - 64 битное число с плавающей точкой
  • usize - беззнаковое целое число, размер которого равен размеру машинного слова
  • char - беззнаковое 8 битное число, используемое для представления символов
  • String - строка, состоящая из символов, представимых типом char
  • Vec<type> - динамический массив, способный хранить примитивные типы (все, кроме String и Vec<T>)

Пример объявления переменных:

let a: i32 = 100;  // иммутабельная переменная a
let mut b: i32 = 100;  // мутабельная переменная b
let mut c: i32;  // мутабельная неинициализированная переменная c
let sum = a + c;  // ошибка, использование неинициализированных переменных (с) запрещено
let d: i32;  // ошибка, иммутабельные переменные должны быть инициализированы сразу при объявлении

let e = 100;  // при объявлении переменных можно не указывать явно тип, в данном случае e имеет тип i32 (type inference)
let mut f;  // ошибка, при использовании type inference переменная должна быть инициализирована сразу при объявлении

Особенностью языка, также заимствованной из Rust, являются довольно строгие правила приведения типов:

  • Типы операндов арифметических операций (+, -, *, /, %) должны совпадать
  • bool - единственный допустимый тип операндов логических операций (&&, ||)
  • Условия в if и while должны иметь тип bool
  • Неявные преобразования типов не совершаются ни в каком случае
  • Для явного преобразования типов используется оператор as: <expression> as <type>. Если запрашиваемое преобразование допустимо, оно будет сделано, иначе - будет брошено исключение
  • Целочисленные литералы отвечают типу i32, литералы с суффиксом usize - типу usize (например, 0 и 0usize)

Условные конструкции и циклы

Условия в if и while не обязательно должны быть в круглых скобках. Цикл for имеет 3 вида:

  • for <variable> in <expr1>..<expr2> - expr1 и expr2 должны иметь одинаковые типы: i32 или usize. variable пройдет все значения от expr1 до expr2 невключительно с шагом 1
  • for <variable> in <expr1>..=<expr2> - expr1 и expr2 должны иметь одинаковые типы: i32 или usize. variable пройдет все значения от expr1 до expr2 включительно с шагом 1
  • for <variable> in <expression> - expression должно иметь тип String или Vec<T>. variable пройдет по значениям итерируемого объекта и будет иметь тип Char или T соответственно. Если expression - это переменная, то на время этого цикла она станет иммутабельной Внутри циклов можно использовать break и continue.

Функции

Функции должны быть объявлены на самом внешнем уровне вложенности, среди всех функций должна быть ровно 1 функция с названием main. Она должна ничего не принимать и ничего не возвращать. Имена всех функций должны быть различны.

Функции могут принимать аргументы, типы аргументов должны быть явно указаны при объявлении. При вызове функции имена аргументов станут переменными, доступными внутри данной функции. Их можно сделать изменяемыми, добавив при объявлении mut: fn foo(a: i32) -> fn foo(mut a: i32).

Функции могут возвращать значения, тип возвращаемого значения должен быть явно указан при объявлении: fn void_function(), fn non_void() -> i32. В теле функции значение может быть возвращено с помощью конструкции return <expression>; в случае функции без возвращаемого значения можно досрочно завершить её исполнение: return;. Также функция может заканчиваться строкой, содержащей выражение без ; в конце, тогда это выражение будет вычислено и возвращено, использование слова return в данном случае необязательно. Пример:

fn sum(a: i32, b: i32) -> i32 {
  a + b
}

При вызове функции все аргументы будут скопированы, возвращаемое значения так же будет скопировано.

Compound типы

В языке есть 2 непримитивных типа: String и Vec<T>. Они обладают некоторыми качествами, не присущими остальным, фундаментальным типам

Создание

Для создания экземляра типа String есть строковые литералы: let foo: String = "bar";. Для создания экземпляра типа Vec<T> есть две конструкции: vec![expr1, expr2, ..., expr_n] и vec![expr1; expr2]. В первом случае будет создан массив из переданных ему элементов; все они должны иметь тип T. Во втором случае будет создан массив из expr2 (должно иметь тип usize) элементов expr1 (должно иметь тип T).

Доступ по индексу

Compound типы позволяют получить доступ по индексу к своим элементам: expr1[expr2], expr1 - compound type, expr2 должен иметь тип usize. Однако в такой формулировке элементы будут доступны только на чтение. Для изменения элементов compound типов есть следующая конструкция: identifier[expr1] = expr2. В переменной identifier должен быть compound тип, хранящий элементы типа U; expr1 должно иметь тип usize, expr2 должно иметь тип U.

Методы

У String есть единственный метод .len() -> usize, который возвращает количество символов в строке. У Vec<T> есть 3 метода: .len() -> usize, .push(T) и .pop().

Методы можно вызывать только от значений, хранящихся в переменных.

Другие функции

Вывод на экран

В языке есть функции print! и println!, позволяющие выводить значения выражений на экран. Есть 3 варианта использования:

  • println!();
  • println!("string"); - вывод строки на экран
  • println!("{}, {}!", expr1, expr2); - форматированный вывод

Области видимости

Trust имеет области видимости переменных и допускает, например, такие конструкции:

fn foo() {
  let bar = 0;
  let bar = 0;
  {
    let bar = 1;
    println!("{}", bar);  // будет выведено 1
  }
  println!("{}", bar);  // будет выведено 0
}

Сходства и различия с Rust

Изначально я хотел придерживаться стратегии, что любая программа, успешно интерпретируемая Trust, должна так же работать и на Rust, но вскоре пришлось отказаться от этого требования. Однако я старался допускать минимальное количество ситуаций, когда это "правило" бы нарушалось, стараясь принимать во внимание здравый смысл. Ниже представлен неполный список случаев, когда программа работает на Trust, но не на Rust:

Строковые литералы

let s: String = "foo";

Эта программа корректна на языке Trust, так как выражение "foo" имеет тип String. В Rust же "foo" имеет тип &str, и чтобы добиться желаемого результата, нужно сделать так:

let s: String = "foo".to_string();

Доступ по индексу у String

let s: String = ...;
println!("{}", s[0]);

Trust выведет первый символ строки s. На Rust же такая программа не скомпилируется. Вот исправленная версия:

let s: String = ...;
println!("{}", s.chars().nth(0).unwrap());

Использование возможно неинициализированных переменных

let mut var: i32;
if true {
  var = 0;
}
println!("{}", var);

Эта программа на Trust выводит 0, но не компилируется на Rust с ошибкой "used binding var is possibly-uninitialized"

Нюансы, связанные с borrow-checker и передачей владения

Следующие программы работают на Trust, но не работают на Rust. Это происходит из-за того, что в тех местах, где в Rust происходит передача владения, в Trust делается копия объекта.

В сущности, эти 3 примера показывают одно и то же явление, но для наглядности приведены они все. Пример 1:

let foo = vec![1, 2];
for element in foo {
  println!("{}", element);
}
for element in foo {
  println!("{}", element);
}

Пример 2:

let foo = vec![1, 2];
let x = foo;
let y = foo;

Пример 3:

fn take_ownership(v: String) {
  ;
}

fn main() {
    let foo: String = "bar";  // .to_string();
    take_ownership(foo);
    take_ownership(foo);
}

Как запускать

  • Склонировать репозиторий
  • mkdir build
  • cd build
  • cmake ..
  • make
  • ./TrustLangInterpreter input.in