Программист
103,4
рейтинг
27 июня 2013 в 17:57

Разработка → Встраиваемый язык для .NET, или как я переспорил Эрика Липперта

Предисловие


Случается, что навязчивая идея так прочно заседает в голове, что ты возвращаешься к ней снова и снова на протяжении многих лет. Пытаешься подойти к проблеме с другой стороны, воспользоваться новыми знаниями, или же просто начать все еще раз с чистого листа — и так до тех пор, пока вопрос не будет исчерпан раз и навсегда. Для меня такой идеей-фикс стали языки программирования. Сам факт того, что одна программа позволяет создавать другие программы, в моих глазах наделял ее непостижимой фрактальной красотой. Написание такой программы самому стало лишь вопросом времени.

В первый раз время наступило после второго курса. Я был уверен, что полученных знаний языка C мне хватит, чтобы в одиночку написать и компилятор, и виртуальную машину, и всю стандартную библиотеку к нему. Задумка была элегантна и дышала романтикой юношеского максимализма, но вместо этого результатом двух лет прилежной работы стало монструозное нечто. Даже несмотря на то, что виртуальная машина подала признаки жизни и смогла исполнить довольно несложные скрипты на псевдоассемблере, который помог написать боевой товарищ fornever, проект был вскоре заброшен. Вместо этого было решено написать язык для платформы .NET, чтобы нахаляву получить автоматическую сборку мусора, jit-компилятор и все прелести огромнейшей библиотеки классов. Компилятор был реализован всего за полгода, исходный код выложен на CodePlex, и с ним я успешно защитил диплом.

Однако чего-то по-прежнему не хватало. При всех своих преимуществах, разработанный для диплома язык требовал явного объявления всех типов и функций, не имел никакой поддержки generic'ов, не умел создавать анонимные функции, да и вообще область его применения была неясна. Решение изобрести еще один велосипед пришло спустя год, когда я дописал игру для Windows Phone и стал думать о том, чем бы заняться дальше. К новому языку были поставлены следующие требования:

  • Взаимодействие с любыми доступными типами .NET без явного импорта
  • Поддержка generic'ов
  • Поддержка анонимных функций и замыканий
  • Наличие хоть какой-нибудь практической ценности

Вышеупомянутый fornever изъявил желание поучаствовать, и работа закипела. Он принимал активное участие в создании дизайна языка и написал парсер на F#, а я занялся описанием синтаксического дерева и внутренней инфраструктуры.

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

Кому нужен очередной велосипед?


Почти три года назад в какой-то из тем на хабре мы выяснили, что языков программирования в мире по-прежнему больше 2500. Зачем кому-то может пригодиться еще один? Что в нем вообще может быть такого, чего нет в других?

Ошеломляющий успех JavaScript и Lua стал поводом сделать язык встраиваемым, с упором на интеграцию с хост-приложениями под .NET. Отсюда же появилось название проекта — LENS — сокращение от Language for Embeddable .NET Scripting. Под «интеграцией» понимается возможность объявить в скрипте тип или функцию, а также обмен непосредственно объектами между внешней и встроенной программами во время выполнения. Например, вот так:

public void Run()
{
	var source = "a = 1 + 2";
	var a = 0;

	var compiler = new LensCompiler();
	compiler.RegisterProperty("a", () => a, newA => a = newA);
        
	try
	{
		var fx = compiler.Compile(source);
		fx();

		Console.WriteLine("Success: {0}", a);
	}
	catch (LensCompilerException ex)
	{
		Console.WriteLine("Error: {0}", ex.FullMessage);
	}
}

Как видно из примера, подключить поддержку LENS очень просто: достаточно добавить сборку в Reference'ы проекта, создать экземпляр и скормить ему исходный код. Вся «магия» заключается в методе RegisterProperty — с его помощью любое значение из программы-хоста может стать доступно в скрипте как на чтение, так и на запись. Для типов и функций существуют методы RegisterType и RegisterFunction соответственно.

Возможности языка


С точки зрения синтаксиса язык LENS почерпнул многое из языков Python и F#. За десять лет работы с C-подобными языками точки с запятой и фигурные скобки набили оскомину, поэтому тут выражения завершаются переносом строки, а блоки выделяются отступами.

Базовые типы

Базовыми типами считаются bool, int, double и string. Константы этих типов записываются так же, как в C#.

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

Переменные объявляются с помощью ключевых слов var и let. Первое объявляет изменяемую переменную, второе — переменную только для чтения.

let a = 42
var b = "hello world"

Управляющие конструкции

Условие записывается с помощью блока if, циклы — с помощью while:

var a = 1
while(a < 10)
    if(a % 2 == 0)
        print "{0} is even" a
    else
        print "oops, {0} is odd" a
    a = a + 1

Управляющие конструкции возвращают значение. Это значит, что if может также использоваться по правую сторону от знака присваивания:

let description = if(age < 21)
    "child"
else
    "grown-up"

Функции

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

print "test"
print a b c
print "result is: " (1 + 2)

Для вызова функции без параметров используется пара пустых скобок. Дело в том, что в функциональной парадигме нет такого понятия, как «функция без параметров». Тру-функциональщики предпочитают оперировать только чистыми функциями, а чистая функция без аргументов по сути является константой. Пара пустых скобок в данном случае — литерал типа unit (синоним void), который обозначает отсутствие аргументов. Аналогичным образом вызывается конструктор без параметров.

Объявление функции начинается с ключевого слова fun:

fun launch of bool max:int name:string ->
    var x = 0
    while(x < max)
        println "{0}..." x
        x = x - 1
    print "Rocket {0} name is launching!" name
    let rocket = new Rocket ()
    rocket.Success    

countdown 10

В LENS нет ключевого слова return. Возвращаемым значением функции является ее последнее выражение. Если функция не должна ничего возвращать, но последнее выражение имеет какой-то тип, используется уже знакомый литерал (). Ключевые слова break и continue также не предусмотрены.

В версии, над которой работаем в данный момент, функцию будет можно автоматически сделать мемоизируемой. Для этого используется ключевое слово pure перед описанием функции. Мемоизируемые функции кешируют свои значения в словаре: если функция уже однажды была вызвана с таким набором параметров, ее значение будет получено из этого словаря, а не вычислено заново:

pure fun add of int x:int y:int ->
    print "calculating..."
    x + y

add 1 2   // output
add 2 3   // output
add 2 3   // no output!

Пользовательские структуры и алгебраические типы

С помощью ключевого слова record можно описать структуру и список ее полей.

record Point
    X : int
    Y : int

let zero = new Point ()
let one = new Point 1 1

Алгебраические типы объявляются ключевым словом type и списком вариантов, которые этот тип может принимать. Вариант может также иметь метку произвольного типа:

type Card
    Ace
    King
    Queen
    Jack
    ValueCard of int

let king = King
let ten = ValueCard 10
print (ten is Card) // true

Для структур создается конструктор по умолчанию и конструктор, инициализирующий все поля сразу. Также для встроенных типов автоматически создаются методы Equals и GetHashCode, позволяющие использовать их в качестве ключей в словарях.

Контейнеры

Для инициализации часто используемых контейнеров используется особый синтаксис оператора new:

let array = new [1; 2; 3; 4; 5]
let list = new [[ "hello"; "world" ]]
let tuple = new (13; 42.0; true; "test")
let dict = new { "a" => 1; "b" => 2 }

Для контейнеров автоматически выводится наиболее подходящий общий тип. Например:

let a = new [1; 2; 3.3] // double[]
let b = new [King; Queen] // Card[]
let c = new [1; true; "hello"] // object[]

Extension-методы

Если в настройках не отключен соответствующий флаг, компилятор будет также искать подходящие методы-расширения:

let a = Enumerable::Range 1 10
let sum = a.Product ()

С помощью несколько хитроумного синтаксиса поддерживается LINQ:

let oddSquareSum = Enumerable::Range 1 100
    |> Where ((x:int) -> x % 2 == 0)
    |> Select ((x:int) -> x ** 2)
    |> Sum ()

Кроме того

В компиляторе реализовано еще много любопытных вещей:

  • Константные выражения вычисляются во время компиляции
  • Поддерживаются переопределенные операторы
  • Поддерживается классическая обработка исключений в виде try\catch
  • Сгенерированную сборку можно сохранить в .exe, если в нее ничего не импортировано
  • Описанные в коде функции можно использовать как extension-методы

Так что там про Липперта?


Многие из дочитавших статью аж досюда наверняка томятся в ожидании — где же обещанная драма? Помню-помню, но сначала лирическое отступление.

Backend'ом для компилятора является замечательная библиотека Reflection.Emit, входящая в состав .NET Framework. Она позволяет на лету создавать типы, методы, поля и прочие сущности, а код методов описывается с помощью команд языка MSIL. Однако наряду с широкими возможностями в ней есть и изрядное количество досадных подводных камней.

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

var intMethods = typeof(int).GetMethods(); // все работает

var myType = ModuleBuilder.DefineType("MyType");
myType.DefineMethod("Test", MethodAttributes.Public);

myType.GetMethods();  // NotSupportedException

На stackoverflow мне доходчиво объяснили, что хранение списка созданных методов, равно как и поиск по ним, придется реализовывать ручками. Трудоемко, зато не сложно.

Но дальше — больше.

Оказалось, что инспектировать нельзя не только созданные типы, но и встроенные generic-типы, которые используют созданные в качестве параметров! Вот пример класса, попытка создать который на Reflection.Emit вызовет проблему:

class A
{
	public List<A> Values = new List<A>();
}

Получается замкнутый круг: получить конструктор типа List<A> можно только тогда, когда сборка уже финализирована и он больше не нужен.

На мой очередной вопрос на Stackoverflow мне ответили Джон Скит (автор книги C# in Depth) и Эрик Липперт (до недавнего времени ведущий разработчик C#). Вердикт Эрика был неутешителен и бесповоротен:

Reflection.Emit is too weak to use to build a real compiler. It's great for little toy compilation tasks like emitting dynamic call sites and expression trees in LINQ queries, but for the sorts of problems you'll face in a compiler you will quickly exceed its capabilities.

Reflection.Emit слишком слаба, чтобы строить на ней настоящий компилятор. Она подходит для «игрушечных» задач компиляции вроде создания динамических вызовов или деревьев выражений в запросах LINQ, но для решения проблем настоящего компилятора ее возможностей быстро перестанет хватать.

По словам Эрика, правильнее всего было бы переписать компилятор с использованием Common Compiler Infrastructure, но этот вариант я даже не рассматривал. Первым пришедшим в голову решением было исключить из языка возможность объявлять собственные типы, но это было бы неспортивно. Чутье подсказывало, что обязательно должен быть какой-то неочевидный способ, позволяющий обойти данное ограничение.

И такой способ действительно был! Он даже оказался куда более очевидным, чем я ожидал.

Как мне подсказали на том же stackoverflow, у класса TypeBuilder есть статические методы, позволяющие получить метод, поле или свойство следующим образом:

var myType = createType("MyType");
var listType = typeof(List<>);
var myList = listType.MakeGenericType(myType);

var genericMethod = listType.GetMethod("Add");
var actualMethod = TypeBuilder.GetMethod(myList, genericMethod);

Тут, однако, есть существенный недостаток: в возвращаемом методе не подставлены типы аргументов. Результатом будет дескриптор метода List<MyType>.Add(T item): тип аргумента будет именно T (generic parameter), а не ожидаемый MyType.

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

Вывод — даже великие иногда ошибаются, а на Reflection.Emit можно сделать полнофункциональный компилятор. Правда, придется как следует попариться.

Если кому-то любопытно узнать больше об ограничениях Reflection.Emit, советую почитать статью в блоге MSDN, написанную еще в 2009 году. Там приводится несколько примеров топологий классов, которые нельзя сгенерировать. Осторожно, примеры на VB!

Чудеса мемоизации


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

  • Проверки возможности приведения выражения к типу
  • Поиска наиболее подходящего overload'а метода
  • Определения наиболее подходящего общего типа для коллекции

Метод этот содержал в себе больше десятка всевозможных проверок и занимал немалую долю от времени компиляции. Но расстояние между двумя типами не меняется со временем, поэтому его вполне можно закешировать в словарь типа Dictionary<Tuple<Type, Type>, int>. Мемоизация трех ключевых методов заняла где-то полчаса и сократила время компиляции нескольких сложных скриптов примерно в 60 раз.

Будущее проекта


В данный момент компилятор работает стабильно и проходит более двухсот тестов. Его уже можно применять в реальных проектах, но это отнюдь не значит, что работа завершена. Основная задача — переписать парсер с F# на C#. Использование библиотеки FParsec для построения парсеров себя не оправдало, и поддерживать изменения в грамматике стало невыносимо. Кроме того, она предоставляет довольно скудные возможности для вывода сообщений об ошибках и тащит за собой весь F# runtime и 500 килобайт зависимостей. Если учесть, что весь код компилятора занимает 250 кб, это очень много.

По этой причине некоторые возможности уже реализованы в компиляторе, однако до сих пор не поддерживаются в парсере — малейшие изменения в грамматике вызывают лавинообразную волну обрушения тестов. Среди таких «фишек» — цикл for/foreach, раздел finally при обработке исключений и мемоизация функций, а также небольшие облагораживания синтаксиса.

В остальном фронт работ примерно следующий:

  • Добавить поддержку сопоставления с образцом (pattern matching)
  • Добавить поддержку инициализаторов объектов
  • Разрешить объявлять обобщенные методы и, возможно, структуры
  • Добавить поддержку подписки на события
  • Описать все возможности в документации

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

Где можно попробовать?


Весь исходный код доступен в репозитории на гитхабе:

github.com/impworks/lens

В проекте есть три тестовых программы-хоста, на которых можно проверить работу компилятора. Для их работы потребуется F# Redistributable. Если у вас установлена Visual Studio 2010 и старше, ничего ставить не нужно.

Собранные демки для Windows

Консоль

Самый простой хост для компилятора. Программа вводится построчно или грузится из файла. Для запуска нужно поставить в конце строки символ #.



Графопостроитель

Позволяет построить график двухмерной функции по ее формуле в виде y = f(x). Можно задать диапазон и шаг.


(Картинки кликабельны)

Графическая песочница

Наиболее функциональное хост-приложение. Оно предоставляет скрипту типы Circle и Rect, которые можно отображать на экране и описывать логику их поведения. В комплекте есть несколько демонстрационных скриптов.



Итого


Все-таки проект делался скорее для развлечения, нежели для решения практических задач. Разумеется, он может никому не пригодиться и заглохнуть, но работа над ним заняла меня интересным делом примерно на восемь месяцев и дала возможность изучить тонкости внутреннего устройства фреймворка, что само по себе здорово. А уж если кому-то он пригодится в реальных проектах — дайте знать!
@impwx
карма
115,0
рейтинг 103,4
Программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (57)

  • +1
    Круто! Языки программирования — моя любимая тема.
    Язык выглядит красиво и юзабельно. Желаю вам продолжать работу над своим детищем.
  • +1
    var a = 1
    while(a < 10)
        if(a % 2 == 0)
            print "{0} is even" a
        else
            print "oops, {0} is odd" a
        a = a + 1
    


    После чтения данное кода, отказ от фигурных скобок мне показался сомнительным.
    • +3
      А что в нем не так? Вроде красиво выглядит. Ещё б в if и while скобки убрать.
      • +3
        Когда будет готов новый парсер, скобок там не будет.
        Такой синтаксис выглядит непривычно, но со временем понимаешь, что и у него есть свои преимущества.
        • +25
          ещё добавить двоеточия в конце условий и определение функций как def -))
          • +10
            Гвидо, перелогинтесь ;)
        • –2
          Может лучше, что бы изначально синтаксис был привычным? И не надо будет «понимать» преимущества.
          Или это мода такая, на каждый проект — свой скриптовый язык?
          • +1
            Изначально привычным для кого? Тем, кто писал на Python или F#, такой синтаксис вполне знаком. А всем сразу угодить нельзя.
      • –6
        Я не знаю что такое «красиво», но код непонятный.
        a = a + 1 — это в теле цикла или уже нет?
        • +2
          Да, в цикле. Это видно по отступу.
          • –2
            Спасибо, кэп. Ладно если намёки не понимаете, скажу прямо: я понимаю, что лавры Python-а спать не дают и всё такое, но наверное не стоило бы вот так прямо тащить наиболее странные идеи, придуманые по обкурке одним известным голландцем.

            Я понимаю, что все минусанувшие — аццкие монстры и легко на глазок определяют четыре в коде отступа или пять, например, но вот в том же MS немного подумали и в F# кроме lightweight cтиля сделали еще и begin… end несмотря на «набившие оскомуну»(ну и аргумент, блин!).
            • 0
              Ну, по одному пробелу на уровень никто не заставляет ставить, а отступы по 4 пробела или по табу вполне хорошо различимы.
              Зато в языках со скобками вообще всё в одну строку можно записать.

              Хотя лучший вариант это дать пользователю выбор синтаксиса. Тут я с вами согласен.
              • 0
                В питоне такая фича была, надо только найти (:
                • 0
                  from __future__ import braces
                  

                  Вы об этом?)
                  • 0
                    Именно (:
            • +1
              Если у вас в коде за 5 уровней вложенности, то у меня для вас плохие новости.
    • +3
      Стоит отметить, что в F# синтаксис в точности такой же. И ничего, никто не жалуется — даже когда-то отказались от синтаксиса со скобками (который был дефолтным) в пользу синтаксиса с отступами и опциональными begin / end.
  • +2
    Основная задача — переписать парсер с F# на C#

    Перепишите лучше с использованием Nemerle.Peg.

    А почему вариант с CCI не рассматривался?
    • 0
      На данный момент мы стараемся как можно больше сократить количество внешних зависимостей. В идеале, чтобы всё было в одной сборке без использования ILMerge — встраиваемый же, всё-таки, язык. Предубеждений против Nemerle не имеем :)
    • 0
      Вариант с CCI не расматривался по двум причинам: необходимость почти все переписать с нуля и внешние зависимости.
  • +1
    Спасибо, интересная подборка средств разработки компиляторов под .NET.

    К сожалению, предполагаемая у меня архитектура (которая только в начале обдумывания) очень далека как от Вашего подхода, так и от начала реализации.
    Но есть пара идей, которые считаю достойными реализации:
    — модель уровней доступа, основанная на модели взаимодействия открытых систем
    — компилятор компиляторов с модульным описанием языка habrahabr.ru/qa/38765/
  • –1
    Очень нужная штука. Надеюсь на серьёзное развитие. Чтобы можно было использовать в production и реализовывать на этих скриптах более сложные и тяжёлые вещи.
    • –1
      Спасибо! Развитие однозначно будет. Если у вас есть какие-то примеры того, что бы хотелось описывать, буду очень рад послушать.

      С другой стороны, описывать особо сложные вещи на встраиваемом языке неправильно. Его основное назначение — быть гибким связующим звеном между другими компонентами. Именно поэтому в языке никогда не планировалась поддержка классов и интерфейсов.
      • –1
        Именно поэтому в языке никогда не планировалась поддержка классов и интерфейсов.

        «Кортежей» (tuples) в принципе будет достаточно.
    • +5
      А чем Вас Iron(%whatever%) языки не устраивают?
    • 0
      А Lua с дотнетом не дружит?
    • +6
      Для скриптов в .NET приложениях есть:

      • IronPython, IronRuby и ещё целый зоопарк Iron* — высочайшее качество, производительность лучше референса, опциональная глубокая интеграция с CLR, интеграция в две строчки;
      • PowerShell — более консольный язык, со специфическими для «команд» фишками, интеграция в две строчки;
      • C#, VB — в представлении не нуждаются, но компиляция дольше, интеграция в десять строчек;
      • Вагон и маленькая тележка других вариантов: Lua, JavaScript, CsScript...

      Многие из этих вариантов произведены большими командами и корпорациями, имеют проверенное временем высочайшее качество и отличную производительность. Не представляю, зачем может понадобиться наколенная поделка бывшего студента, для которой написано «аж 200 тестов» и который гордится тем, что не последовал разумному совету одного из разработчиков C#, «потому что переписывать лень».

      Написание своего языка — это, конечно, круто и интересно, но всё-таки забава в чистом виде, о чём сам автор в послесловии и говорит.
      • +1
        Полагаю, остальные поделки, проверенные опытом, начинались точно так же. Иначе хорошие вещи не пишутся. Для себя и в своё же удовольствие.
        • +2
          Чувак, который стоит у истоков Jython и IronPython, уже имел за плечами хороший багаж, это далеко не первые наколенные проекты. Чувак, который ваяет IronScheme, уже имеет за плечами заваленный IronLisp. *Lua* и *J*Script* — это обёртки над уже существующими реализациями (Lua, JScript, V8). В общем и целом, практически во всех случаях мы имеем реализации уже существующих языков (с некоторыми дополнениями, специфическими для платформ JVM, IL и др.), причём обычно разработчиками с богатым опытом. Из перечисленных языков изобретён с нуля разве что PowerShell, но с точки зрения программирования не для консоли язык — ужасен, если интересно моё мнение. :)

          Вот когда автор выкинет два-три проекта (один выкинутый уже есть, второй на подходе) — уже может начать получаться что-то удобоваримое. Но для этого надо хотя бы начать слушать старших прислушиваться к мнению Эрика.
      • 0
        Если не вдаваться в холивар, то у любого решения есть свои плюсы и минусы. Если учитывать переносимость, скорость работы и объем требуемых зависимостей, может статься, что и поделка найдет свое применение.

        Кстати, а почему вы употребили фразу «бывший студент» в негативном смысле? Мне стоит стыдиться того, что я закончил вуз?
        • 0
          C# уже встроен в фреймворк, поэтому переносимость 100% (.NET, Mono), скорость работы 100% (в рамках CLR), зависимостей нет (.NET или есть весь, или его нет вообще). Учитывая, что сам .NET весит метров 40, экономия в два мегабайта лишних зависимостей совершенно неуместна. Соревноваться с DLR по производительности в принципе возможно, но разница будет пренебрежимо мала. Скорее же всего, вам не хватит времени на оптимизацию реально всех случаев, поэтому побить DLR удастся в лучшем случае в 5% случаев. Команде IronPython, чтобы добиться производительности CPython, потребовалось несколько лет активной разработки, и то осталось несколько узких мест.

          В свете этого реализация «ещё одного языка» полностью лишена практического смысла (разве что ради опыта). Новый язык реализуют тогда, когда ни один уже имеющийся не подходит. C# возник как «Java, но мощнее»; PowerShell — как конкуренция консоли *никсов; Go — как эффективная реализация динамического языка; F# — как функциональный язык для .NET. И так далее. А что у вас в требованиях перечислено — это всё уже давно есть.
  • +5
    Вывод — даже великие иногда ошибаются, а на Reflection.Emit можно сделать полнофункциональный компилятор. Правда, придется как следует попариться.
    У System.Reflection.Emit есть один фатальный недостаток — он генерирует код только под ту версию рантайма, под которой запущен компилятор. С этим в частности столнулись разработчики упомянутого выше Nemerle. В результате нужно иметь отдельную версию компилятора под каждую версию .NET
    Для встаеваемого языка это не важно, но для компиляторов «обычных» языков, типа C#/F#/VB.NET — это серьезный минус.
    • 0
      Не понимаю что тут такого страшного.
      • 0
        Ну, это страшно, если вы хотите реализовать кросскомпиляцию — с помощью компилятора под .NET4 делать бинарники для .NET2, например. Не уверен, насколько всё плохо в этом плане для пары 4 / 4.5, но, скорее всего, тоже не ахти.
        • 0
          т.к. язык для встраивания, то работать программа будет заведомо только на той версии рантайма, на которой запущен хост, поэтому ни о какой кросскомпиляции речи не идет
  • +2
    Еее, DynamicDataDisplay! :)
    • +2
      Отличная штука! В предыдущем компиляторе тоже использовал ее для вывода графиков. Сейчас почитаю вашу статью про нее — судя по скриншотам, я знал только об одном проценте возможностей :)
      • 0
        Эх, только какой это старичок уже. Я не следил пару лет за тем, что делается в этой области, наверное, уже появились другие более мощные и бесплатные библиотеки. Однако периодически народ пишет на почту, что-то спрашивает, я даже что-то вспоминаю :)
  • 0
    А каким компонентом выводятся графики в примере?
    • 0
      Прошу прощения, не прочитал комментарий выше.
  • 0
    Тут бы ещё и про Наггум написать :)

    Статья однозначно замечательная, спасибо.
    • 0
      В каком состоянии сейчас проект? Наверняка есть про что написать хотя бы небольшую статью.
  • +1
    Ох тыж, у меня прямо сейчас задача для LENS есть!
  • 0
    А мне довелось делать скриптовый движок в одном проекте на базе .Net Framework 2.0,
    синтаксис простейший — как у формул в Excel (собственно, в ячейках xls-документа и предполагалось записывать выражения),
    но для удобства написания «невнутриэксельных» скриптов добавлены операторы:
    * . (точка, передаёт левое выражение в качестве первого аргумента вызываемой справа функции);
    * .. (двоеточие, аналогично, только вторым аргументом);
    * а также объявлений массивов "{… }" и выборки по индексу "[n]".

    Формируется «шитый» код с вызовом «пользовательских» функций (синхронные/асинхронные, с побочными эффектами и без, макрофункции, всё реализовано через вызовы функций), поддерживается «ленивость», «асинхронность» (на основе собственного «велосипеда» по мотивам шаблона AsyncEnumerator), «прозрачное» применение скалярных функций к IList-векторам (например, "{1,2,3}.sin()"). Язык безтиповый, все значения передаются как Object.
    Все именованные значения вычисляются однократно и «лениво», константные выражения вычисляются при «компиляции», имеется базовая поддержка замыканий.

    Из недостатков: .Net 2.0 (в 4-м можно было бы использовать встроенные возможности для реализации асинхронности), сложный код писать трудновато без интеллектуального редактора, также приходится ограничивать параллелизм семафорами, иначе память кушается много))

    Изначально задумывался «движок» для построения отчётов/организации вычислений на основе шаблонов/алгоритмов, описанных в Excel-документе, без использования ПО Excel по следующему принципу: на основе Excel-шаблона внутрипрограммно формируется скрипт для построения отчёта, после чего «компилируется»/запускается, запрашивает требуемые параметры и делает отчёт/вычисления в фоновом режиме.

    Пример «внешнего» скрипта:
    (
    	let(endTime, NOW()),
    	let(begTime, endTime-366*3),
    	let(strFmt, '{0}' & CHAR(9) & '{1}' & CHAR(9) & '{2}' & CHAR(13) & CHAR(10)),
    	let(semaObj2Txt, Semaphore(256)),
    	let(wells,
    		ReadRaw(treeAction,'spr@all@full_well')
    			.RawToFullWellTable()
    			.SelectRows('fieldId=110001')
    			.RowAsDict()
    	),
    	let(fLPV, func(well,paramInfo, LoadParamValues(treeAction, well.FullWellItemToWellPath(), paramInfo, begTime, endTime) )),
    	_ForEach(
    		paramInfo, ParamStorage(),
    		(
    			let(outFile, NewFileStreamWriter('Out\' & paramInfo & '.tsv')),
    			outFile.StreamWriterAppendText('WellId' & CHAR(9) & 'Time' & CHAR(9) & 'Value'),
    			_ForEach(
    				well, wells,
    				_EvalWithSemaphore
    				(	semaObj2Txt,
    
    					let( wellId, well['wellId'] ),
    					fLPV(well, paramInfo) .. let(vector),
    					IF(	AND( NotNull(vector), COLUMNS(vector)>0 ),
    						(
    							outFile.StreamWriterAppendText( _JoinStr(CHAR(13) & CHAR(10) & wellId & CHAR(9), vector) ),
    							COLUMNS(vector)
    						),
    						0
    					)
    				)
    			)
    			.SUM()..let(sum),
    			outFile.StreamWriterClose(),
    			sum
    		)
    	)..let(result),
    	{
    		start(), 
    		COLUMNS(result), SUM(ISNUMBER(result)), SUM(result), MAX(result), MIN(result), 
    		_TimeOf(result).MAX()._TimeToStr(), _TimeOf(result).MIN()._TimeToStr(), 
    		result.stop() 
    	}
    )
    
    • 0
      Оно именно компилируется в байткод .net, не интерпретируется?
      • 0
        Оно компилируется в «дерево» вызовов функций, вычисляемых в заданном контексте.
        Т.е., по факту, никакого нового кода не создаётся, лишь структуры данных, связывающие функции между собой через входы/выходы.
        Больше всего похоже на "Шитый код".
  • +1
    let description = if(age < 21)
    «child»
    else
    «grown-up»

    Это аналог нижеследующего выражения?
    var description = age < 21? «child»: «grown-up»;

    Мне кажется что вряд ли стало более удобочитаемо.
    • –1
      Можно записать и в одну строку:
      let description = if(age < 21) "child" else "grown-up"
      
  • 0
    У меня вопрос любителям. Как видно, на планете есть тысячи языков, которые компилятся в одни и теже машинные коды. Меня всвязи с этим волнует вопрос: есть ли какой-то более-менее общий язык, который легко компилится во все эти тысячи?
    Пояснение: я пишу специфическую библиотеку, например, алгоритм, который нужен редко и малому кругу лиц, а все эти лица пишут свои хорошие программы на всяких разных языках, получается что алгоритм придется кодить на всех тех языках, а я не хочу. Я хочу написать его на самом примитивном языке с самыми примитивными фичами, но так, чтобы по нажатию кнопки всё расплодилось в тысячу исходников на разных языках.
    • 0
      Есть пара языков, которые транслируются в другие: Haxe и Monkey.
      Если кто-то знает ещё такие, напишите.
      • 0
        У F# есть такая возможность как цитирование, позволяющее манипулировать кодом в runtime и, например, скомпилировать его в JS или выполнить на GPU.
        • 0
          Цитирование, наверно, не похоже на то, что мне надо. Я не смог понять что это:)
      • 0
        Начал смотреть про Манкей, на русском сайте как-то странно шутят:

        Важно! С помощью данной версии вы сможете собирать только HTML5 версии игр.
        Для получения возможности сборки под все доступные платформы вам необходимо приобрести лицензию на официальном сайте.
        Стоимость лицензии составляет $99.
        • 0
          Лицензия на 2-d framework вроде. Сам Monkey бесплатен. Хорошо написано тут
    • 0
      В похожей ситуации мне порекомендовали препроцессор M4 Кернигана+Ритчи

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.