Давайте просто посмотрим на структуру кода

import Console;

// Prints the Fibonacci numbers less than `limit`.
fn Fibonacci(limit: i64) {
  var (a: i64, b: i64) = (0, 1);
  while (a < limit) {
    Console.Print(a, " ");
    let next: i64 = a + b;
    a = b;
    b = next;
  }
  Console.Print("\n");
}

Carbon — это язык, который должен быть знаком разработчикам C++ и C.

Функция, которая мне очень нравится:

Весь исходный код представляет собой текст в кодировке UTF-8. Комментарии, идентификаторы и строки могут содержать символы, отличные от ASCII.

var résultat: String = “Succès”;

строка комментария

// Compute an approximation of π

Примитивные типы

  • Bool - логический тип с двумя возможными значениями: True и False.
  • Int и UInt - знаковые и беззнаковые 64-битные целочисленные типы.
  • Доступны стандартные размеры, как со знаком, так и без знака, включая i8, i16, i32, i64, i128 и i256.
  • Переполнение в любом направлении является ошибкой.
  • Float64 — тип с плавающей запятой с семантикой на основе IEEE-754.
  • Доступны стандартные размеры, включая f16, f32 и f128.
  • BFloat16 также предоставляется.
  • String — последовательность байтов, рассматриваемая как содержащая текст в кодировке UTF-8.
  • StringView — доступная только для чтения ссылка на последовательность байтов, рассматриваемую как содержащую текст в кодировке UTF-8.

Кортежи

Кортеж — это набор значений фиксированного размера, которые могут иметь разные типы, где каждое значение идентифицируется по его положению в кортеже. Примером использования кортежей является возврат нескольких значений из функции:

fn DoubleBoth(x: i32, y: i32) -> (i32, i32) {
return (2 * x, 2 * y);
}

  • Тип возвращаемого значения представляет собой кортеж из двух типов i32.
  • Выражение использует синтаксис кортежа для построения кортежа из двух значений i32.

Типы структур

У углерода также есть структурные типы, члены которых идентифицируются по имени, а не по положению. Они называются структурными классами данных, также известными как структурные типы или структуры.

И типы структур, и значения записываются внутри фигурных скобок ({...}). В обоих случаях у них есть список элементов, разделенных запятыми, который начинается с точки (.), за которой следует имя поля.

  • В типе структуры за именем поля следует двоеточие (:) и тип, например: {.name: String, .count: i32}.
  • В значении структуры, называемом литералом класса структурных данных или литералом структуры, за именем поля следует знак равенства (=) и значение, как в {.key = "Joe", .count = 3}.

Типы указателей

Тип указателей на значения типа T записывается как T*. Углеродные указатели не поддерживают арифметику указателей; единственным указателем операции являются:

  • Разыменование: учитывая указатель p, *p дает значение p указывает на l-значение. p->m — это синтаксический сахар для (*p).m.
  • Address-of: учитывая l-значение x, &x возвращает указатель на x.

Массивы и срезы

Тип массива, содержащего 4 значения i32, записывается как [i32; 4]. Существует неявное преобразование кортежей в массивы той же длины, если каждый компонент кортежа может быть неявно преобразован в целевой тип элемента. В случаях, когда размер массива может быть выведен, его можно опустить, например:

var i: i32 = 1;
// `[i32;]` equivalent to `[i32; 3]` here.
var a: [i32;] = (i, i, i);

Выражения

Выражения описывают некоторое вычисляемое значение. Простейшим примером может быть буквальное число, такое как 42: выражение, которое вычисляет целочисленное значение 42.

Выражения — это части синтаксиса Carbon, которые производят значения. Поскольку типы в Carbon являются значениями, это включает в себя все, что указано в типе.

fn Foo(a: i32*) -> i32 {
  return *a;
}

Здесь тип параметра i32*, тип возвращаемого значения i32 и операнд *a оператора return — все это выражения.

Объявления, определения и области действия

Объявления привязки имени:

Существует два типа объявлений привязки имен:

  • объявления констант, представленные с помощью let, и
  • объявления переменных, введенные с помощью var.

Для них нет предварительных объявлений; все объявления привязки имен являются определениями.

Объявления констант let:

Объявление let сопоставляет неопровержимый шаблон со значением. В этом примере имя x связано со значением 42 с типом i64:

let x: i64 = 42;

Здесь x: i64 — это шаблон, за которым следует знак равенства (=) и значение для сопоставления, 42. Имена из шаблонов привязки вводятся в объемлющую область.

Объявления переменных var:

Объявление var похоже, за исключением привязок var, поэтому x здесь является l-значением с хранилищем и адресом, поэтому его можно изменить:

var x: i64 = 42;
x = 7;

Переменные с типом, имеющим несформированное состояние, не нужно инициализировать в объявлении переменной, но их нужно присваивать перед использованием.

auto

Если auto используется в качестве типа в объявлении var или let, тип является обязательным статическим типом выражения инициализатора.

Функции

Базовое определение функции может выглядеть так:

fn Add(a: i64, b: i64) -> i64 {
  return a + b;
}

Это объявляет функцию с именем Add, которая принимает два параметра i64, первый из которых называется a, а второй — b, и возвращает результат i64. Он возвращает результат сложения двух аргументов.

С++ может объявить то же самое:

std::int64_t Add(std::int64_t a, std::int64_t b) {
  return a + b;
}
// Or with trailing return type syntax:
auto Add(std::int64_t a, std::int64_t b) -> std::int64_t {
  return a + b;
}

Пункт возврата:

Предложение return функции указывает тип возвращаемого значения с использованием одного из трех возможных синтаксисов:

  • ->, за которым следует выражение, например i64, прямо указывает тип возвращаемого значения. Это выражение будет оцениваться во время компиляции, поэтому оно должно быть допустимым в этом контексте.
  • Например, fn ToString(val: i64) -> String; имеет возвращаемый тип String.
  • ->, за которым следует ключевое слово auto, указывает, что для определения возвращаемого типа следует использовать вывод типа.
  • Например, fn Echo(val: i64) -> auto { return val; } будет иметь возвращаемый тип i64 посредством вывода типа.
  • Объявления должны иметь известный тип возвращаемого значения, поэтому auto недействителен.
  • Функция должна иметь ровно один оператор return. Выражение этого оператора return затем будет использоваться для вывода типа.
  • Пропуск указывает, что тип возвращаемого значения — пустой кортеж ().
  • Например, fn Sleep(seconds: i64); похоже на fn Sleep(seconds: i64) -> ();.
  • () похож на возвращаемый тип void в C++.

return заявлений:

Оператор return необходим для управления потоком функций. Он завершает поток функции и возвращает выполнение вызывающей стороне.

Когда предложение return опущено, оператор return не имеет аргумента-выражения, и поток управления функцией неявно завершается после последнего оператора в теле функции, как если бы присутствовал return;.

Когда предоставляется предложение return, в том числе когда оно равно -> (), оператор return должен иметь выражение, преобразуемое в тип возвращаемого значения, а оператор return должен использоваться для завершения потока управления функцией.

Объявления функций:

Функции можно объявлять отдельно от определения, предоставляя только подпись без тела. Это предоставляет API, который может быть вызван. Например:

// Declaration:
fn Add(a: i64, b: i64) -> i64;
// Definition:
fn Add(a: i64, b: i64) -> i64 {
  return a + b;
}

Соответствующее определение может быть предоставлено позже в том же файле или, когда объявление находится в api файле библиотеки, в impl файле той же библиотеки. Подпись объявления функции должна соответствовать соответствующему определению. Это включает в себя оговорку о возврате; даже если пропущенный возвращаемый тип имеет поведение, эквивалентное -> (), наличие или отсутствие должны совпадать.

Вызовы функций:

Вызовы функций используют идентификатор функции для передачи нескольких аргументов выражения, соответствующих параметрам сигнатуры функции. Например:

fn Add(a: i64, b: i64) -> i64 {
  return a + b;
}
fn Run() {
  Add(1, 2);
}

Здесь Add(1, 2) — это выражение вызова функции. Add относится к идентификатору определения функции. Аргументы в скобках 1 и 2 передаются параметрам a и b функции Add.

Параметры

Привязки в списке параметров по умолчанию имеют значение let привязки, поэтому имена параметров обрабатываются как r-значения. Это подходит для входных параметров. Эта привязка будет реализована с использованием указателя, если только копирование не является законным, а копирование дешевле.

Если перед привязкой добавить ключевое слово var, то аргументы будут скопированы (или перемещены из временного хранилища) в новое хранилище и, таким образом, могут быть изменены в теле функции. Копия гарантирует, что любые мутации не будут видны вызывающей стороне.

Используйте тип параметра указатель для представления параметра ввода/вывода, позволяя функции изменять переменную вызывающей стороны. Это делает возможность этих модификаций видимой: путем получения адреса с помощью & в вызывающем объекте и разыменования с использованием * в вызываемом объекте.

Выходы функции должны быть возвращены. Несколько значений могут быть возвращены с использованием типа кортеж или структура.

auto тип возврата:

Если вместо возвращаемого типа используется auto, возвращаемый тип функции выводится из тела функции. Он установлен в общий тип статического типа аргументов return операторов в функции. Это не разрешено в предварительном объявлении.

// Return type is inferred to be `bool`, the type of `a > 0`.
fn Positive(a: i64) -> auto {
  return a > 0;
}

Блоки и операторы

блок — это последовательность операторов. Блок определяет область и, как и другие области, заключен в фигурные скобки ({...}). Каждый оператор заканчивается точкой с запятой или блоком. Выражения и var и let являются допустимыми выражениями.

Тело функции определяется блоком, а некоторые операторы потока управления имеют собственные блоки кода. Они вложены в охватывающую область. Например, вот определение функции с блоком операторов, определяющих тело функции, и вложенным блоком как часть оператора while:

fn Foo() {
  Bar();
  while (Baz()) {
    Quux();
  }
}

Операторы присвоения

Операторы присваивания изменяют значение l-value, описанное в левой части присваивания.

  • Назначение: x = y;. x присваивается значение y.
  • Увеличение и уменьшение: ++i;, --j;. i устанавливается на i + 1, j устанавливается на j - 1.
  • Составное присвоение: x += y;, x -= y;, x *= y;, x /= y;, x &= y;, x |= y;, x ^= y;, x <<= y;, x >>= y;. x @= y; эквивалентно x = x @ y; для каждого оператора @.

В отличие от C++, эти присваивания являются операторами, а не выражениями, и не возвращают значения.

Поток управления

Блоки операторов обычно выполняются последовательно. Операторы потока управления дают дополнительный контроль над потоком выполнения и над тем, какие операторы выполняются.

Некоторые операторы потока управления включают блоки. Эти блоки всегда будут заключены в фигурные скобки {...}.

// Curly braces { ... } are required.
if (condition) {
  ExecutedWhenTrue();
} else {
  ExecutedWhenFalse();
}

if и else

if и else обеспечивают условное выполнение операторов. Синтаксис:

if (логическое выражение) {инструкции}

[ else if (логическое выражение) {операторы} ] ...

[ else {утверждения} ]

Будет выполнена только одна группа операторов:

  • Когда первое логическое выражение if оценивается как истинное, будут выполняться связанные с ним операторы.
  • Когда более ранние логические выражения оцениваются как ложные, а логическое выражение else if оценивается как истинное, будут выполняться связанные с ним операторы.
  • ... else if ... эквивалентно ... else { if ... }, но без видимой вложенности фигурных скобок.
  • Когда все логические выражения оцениваются как ложные, будут выполняться связанные операторы else.

Когда логическое выражение оценивается как истинное, никакие последующие логические выражения не оцениваются.

Обратите внимание, что else if может повторяться.

Например:

if (fruit.IsYellow()) {
  Print("Banana!");
} else if (fruit.IsOrange()) {
  Print("Orange!");
} else if (fruit.IsGreen()) {
  Print("Apple!");
} else {
  Print("Vegetable!");
}
fruit.Eat();

Этот код будет:

  • Оцените fruit.IsYellow():
  • Когда True, вывести Banana! и возобновить выполнение в fruit.Eat().
  • Когда False, оценить fruit.IsOrange():
  • Когда True, вывести Orange! и возобновить выполнение в fruit.Eat().
  • Когда False, оценить fruit.IsGreen():
  • Когда True, вывести Orange! и возобновить выполнение в fruit.Eat().
  • Когда False, вывести Vegetable! и возобновить выполнение в fruit.Eat().

while

Цикл операторов while до тех пор, пока переданное выражение возвращает True. Синтаксис:

while (логическое выражение) {инструкции}

Например, это печатает 0, 1, 2, затем Done!:

var x: Int = 0;
while (x < 3) {
  Print(x);
  ++x;
}
Print("Done!");

for

Операторы for поддерживают зацикливание на основе диапазона, обычно по контейнерам. Синтаксис:

for (объявление varinвыражение) {инструкции}

Например, это печатает все имена в names:

for (var name: String in names) {
  Print(name);
}

PrintNames() печатает каждое String в names List в порядке итерации.

break

Оператор break немедленно завершает цикл while или for. Выполнение возобновится в конце области действия цикла. Синтаксис:

break;

Например, это обрабатывает шаги до тех пор, пока не будет выполнен шаг вручную (если ни один шаг вручную не будет выполнен, обрабатываются все шаги):

for (var step: Step in steps) {
  if (step.IsManual()) {
    Print("Reached manual step!");
    break;
  }
  step.Process();
}

continue

Оператор continue сразу переходит к следующему циклу оператора while или for. В while выполнение продолжается с выражения while. Синтаксис:

continue;

Например, это печатает все непустые строки файла, используя continue для пропуска пустых строк:

var f: File = OpenFile(path);
while (!f.EOF()) {
  var line: String = f.ReadLine();
  if (line.IsEmpty()) {
    continue;
  }
  Print(line);
}

match

match представляет собой поток управления, аналогичный switch в C и C++, и отражает аналогичные конструкции в других языках, таких как Swift. За ключевым словом match следует выражение в круглых скобках, значение которого сопоставляется с объявлениями case, каждое из которых содержит опровержимый шаблон по порядку. За опровержимым шаблоном может дополнительно следовать выражение if, которое может использовать имена из привязок в шаблоне.

Выполняется код для первого совпадения case. Необязательный блок default может быть помещен после объявлений case, он будет выполнен, если ни одно из объявлений case не совпадает.

Пример match:

fn Bar() -> (i32, (f32, f32));
fn Foo() -> f32 {
  match (Bar()) {
    case (42, (x: f32, y: f32)) => {
      return x - y;
    }
    case (p: i32, (x: f32, _: f32)) if (p < 13) => {
      return p * x;
    }
    case (p: i32, _: auto) if (p > 3) => {
      return p * Pi;
    }
    default => {
      return Pi;
    }
  }
}