Spiiin's blog

Daslang do_with

Повайбкодил немного на daslang на джеме, сделал вот такую игру.

Наткнулся на то, что ИИ регулярно спотыкается об одну особенность синтаксиса, решил посмотреть, как ему можно помочь с этим.

struct A {
    f1: int
    f2: int
}

[export]
def main {
    // Есть массив структур
    var arrayOfA = [
        A(f1=1, f2=2),
        A(f1=3, f2=4),
    ]

    // хотим поменять поля первой структуры через ссылку
    var a : A& = arrayOfA[0]
    a.f1 = 5
    a.f2 = 6
}

Компилятор ругается тут:

c:\src\daScript\bin\Debug>daslang.exe base_sample.das
error[31300]: local reference to non-local expression is unsafe
base_sample.das:15:8
    var a : A& = arrayOfA[0]

Программисты на C++ могут немного удивиться, но Daslang считает этот код небезопасным, потому что между получением ссылки и обращением к структуре через эту ссылку можно изменить arrayOfA:

var a : A& = arrayOfA[0]
// массив может перевыделить память - push/resize/erase/clear могут сделать ссылку висячей
arrayOfA |> push(A(f1=10, f2=20))
a.f1 = 5

Какие более безопасные варианты обращения к первому элементу массива могут быть?

Отбросим сразу варианты обращения по полному имени (раздувает код) и копирования всей структуры (пессимизация с двойным копированием на ровном месте).

Хранение структур в куче

struct A {
    f1: int
    f2: int
}

[export]
def main {
    // Есть массив структур
    var arrayOfA = [
        new A(f1=1, f2=2),
        new A(f1=3, f2=4),
    ]

    var a : A? = arrayOfA[0]
    a.f1 = 5
    a.f2 = 6

    arrayOfA |> clear

    //print("arrayOfA[0].f1 = {arrayOfA[0].f1}, arrayOfA[0].f2 = {arrayOfA[0].f2}\n")
    print("a.f1 = {a.f1}, a.f2 = {a.f2}\n")
}

Цена — дополнительный уровень индирекции, многовато ради простого удобства синтаксиса, хоть и не самый плохой вариант.

With

Для того, чтобы не писать имя переменной, задумано выражение with:

with (arrayOfA[0]) {
    f1 = 5
    f2 = 6
}

Но оно не подходит, если нужно передать структуру в функцию:

with (arrayOfA[0]) {
    set_f1(arrayOfA[0]) //нет сокращенного имени
    set_f2(arrayOfA[0])
}

Assume
Есть ещё абсолютно ужасный assume:

assume a0 = arrayOfA[0]
set_f1(a0)
set_f2(a0)

Он подходит по синтаксису, но опасен тем, что вычисляет выражение каждый раз. Т.е. имеет все те же эффекты, что и define в C++ — взорвётся не только при перевыделении памяти arrayOfA, но и если выражение в assume имеет побочные эффекты или зависит от других выражений:

var i = 0
assume a0 = arrayOfA[i]

set_f1(a0)
i += 1
set_f2(a0) //обращение к другой структуре из-за изменения i

Т.е. assume просто сам по себе даже более опасен, что ссылки, но dascript не требует помечать его unsafe.

Передача ссылок в функцию

def do_with(var self: A&; b : block<(var a : A): void>) {
    invoke(b, self)
}

def do_with(var self: A&; b : block<(var a : A)>) {
    return invoke(b, self)
}

let f1 = do_with(arrayOfA[0]) $(var a : A) {
    a.f1 = 42
    return a.f1
}
do_with(arrayOfA[0]) $(var a : A) {
    a.f2 = 43
}
print("{f1}, {arrayOfA[0].f2}")

Такой вариант не самый красивый по синтаксису, зато хотя бы безопаснее assume.

Если arrayOfA глобальный, то всё ещё можно изменить его внутри самого блока, переданного в do_with. Это уже не поймает компилятор — нет способа выразить или проверить время жизни массива и ссылки, как делает rust. Тут в любом случае нужно поддерживать ограничение на изменение глобального массива (например, введя правило — менять его только через очередь сообщений).

Syntax matters

В kotlin есть такой синтаксис для scope-функций:

with(arrayOfA[0]) {
    f1 = 5
    println(this)
}

В daslang можно сделать макрос, который также позволит сэкономить символы:

let f1 = with_(arrayOfA[0]) {
    _.f1 = 42
    return _.f1
}

with_ (arrayOfA[0]) {
    _.f2 = 43
}

print("{f1}, {arrayOfA[0].f2}")

Работает это за счёт синтаксического сахара, описанного тутLast block pipes itself. Теперь синтаксис выглядит также, как и выражение with, встроенное в язык.