Обновить

Почему синтаксис Nova выглядит так: 11 споров с Claude о дизайне языка

Уровень сложностиСредний
Время на прочтение11 мин
Охват и читатели8.6K
Всего голосов 4: ↑2 и ↓20
Комментарии6

Комментарии 6

Аффтар, ты клауд убери уже и перепиши всë сам (ну и с Интернетом, но не с ИИ) - волшебное чувство будет, когда ты меняешь всего 5-6 строк - и синтатикс меняется без клаудов))))

Согласен, менять синтаксис своими руками приятно. Claude мне нужен, чтобы обсудить варианты, а потом обновить спецификацию, документацию, компиляторы и тесты — это десятки правок.

В примере ro p *mut int запись через p.write(1) разрешена. А что происходит, если такой указатель захватывают две задачи рантайма? Неизменяемость самой привязки здесь не защищает общий объект от одновременных записей. Запрещается ли такой захват системой типов или синхронизацию должен обеспечить вызывающий код?

Рядом с таблицей ro/mut пригодился бы пример с алиасами: одна задача получает *T, другая — *mut T на тот же объект. Если такая комбинация допустима, показать нужную синхронизацию; если нет — ошибку компилятора. Вы уже оговариваете, что ro останавливается на *, а такой пример отделил бы права конкретного указателя от правил передачи и совместного использования данных между задачами.

Спасибо, вы копнули реально глубоко и нашли весьма интересный момент.

ro в Nova запрещает писать через эту привязку, но не обещает, что данные не изменит кто-то другой: со сборщиком мусора на тот же объект может смотреть mut-привязка или *mut. В спецификации это записано как сознательный компромисс: без проверки заимствований, как в Rust, статически не узнать, кто ещё держит объект, поэтому ro ограничивает только свой путь к данным.

Поэтому для задач ro защитой не считается. Указатель и любой тип с ним делить между задачами нельзя, захват через mut компилятор отклоняет. Общие данные передаются через каналы, Mutex и атомики. А вот ro-захват компилятор сейчас пропускает, не проверяя тип, так что ваш пример скомпилируется, а по задумке Nova должна такие моменты ловить. Это ошибка в спецификации, задачу на неё я уже завёл: ro-захват будет проверяться так же, как mut, а ваш пример с T и mut T войдёт в тесты.

Если найдёте что-то ещё, пишите issue на GitHub. Если находка похожа на уязвимость, лучше приватно, по SECURITY.md.

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

ro r *int = ...       // указатель на счётчик, только чтение
ro w *mut int = ...   // указатель на тот же счётчик, запись
supervised {
    spawn { println(r.read()) }
    spawn { w.write(1) }   // ошибка: указатель нельзя делить между задачами
}

Сейчас компилятор это пропускает, это и есть найденная вами дыра. Общие данные нужно передавать через тип с синхронизацией внутри:

mut c = AtomicInt.new(0)
supervised {
    spawn { println(c.load()) }
    spawn { c.store(1) }
}

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

С интересом слежу - на каком же языке будет удобнее писать с ИИ - на условной Java или на условном Rust. Оба варианта имеют за и против. В Нове предложен третий вариант - скрестить ужа с ежом =)

Пару наблюдений:

  • читать код придется больше, чем читать, а с ИИ - гораздо больше. Птичий синтаксис в этом никак не помогает

  • схема эксклюзивного владения в Расте провалилась - слишком неудобно, приходится городить костыль на костыле, причем даже с компилер мэджик. Здесь же нечто среднее? Не очень понял, тема размазана по док-ции

  • наличие или отсутствие ООП - вопрос спорный, лучше иметь возможность выбора

  • атрибуты функций тоже спорный вопрос - есть примеры, что приводит к замусориванию сигнатур и "шторму" при всякого рода перегрузках

Про чтение согласен, и с ИИ это ещё важнее. Nova с самого начала делалась под схему «ИИ пишет, человек проверяет»: имена полными словами, набор операторов закрыт, эффекты пишутся словами. Из символов есть @ вместо self. и * у указателей. Если какая-то запись режет глаз, напишите, какая: синтаксис уже не раз менялся после таких замечаний.

Владения как в Rust нет, проверки заимствований тоже. Память под сборщиком мусора, объекты передаются свободно, как в Go. Линейность включается только для типов-ресурсов: если тип объявлен, как consume - type File consume { … }, файл нужно закрыть на каждом пути выхода, иначе компилятор не соберёт программу. Либо компилятор закроет сам при реализации @cleanup. Это файлы, транзакции, блокировки, а не всё подряд. Тут вы правы, в документации это разбросано по разным местам.

Классов и наследования нет намеренно. Есть методы на типах, структурные протоколы, композиция и динамическая диспетчеризация через протокол, ближе к Go, чем к Java.

Эффекты пишут не везде. У приватных функций компилятор выводит их сам, обязательны они только у экспортируемых. В лямбдах эффекты не пишут никогда. Если параметр функции имеет тип fn() -> T, туда можно передать функцию с любыми эффектами, так что map не перечисляет эффекты f. В ключ перегрузки эффекты не входят, поэтому перегрузки из-за них не множатся.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации