MoonScript - Компилятор для Igdrasil Engine

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

Основные возможности MoonScript

✅ Компиляция в чистый Lua код
✅ Объектно-ориентированное программирование с классами
✅ Списковые включения (list comprehensions)
✅ Деструктуризация переменных
✅ Условные выражения и switch/case
✅ Упрощённый синтаксис функций
✅ Автоматическое определение локальных переменных

Компилятор MoonScript

Используйте класс MoonScriptCompiler для компиляции MoonScript кода в Lua:

using IgdrasilEngine.Engine.Scripting.Lua;

// Компиляция MoonScript в Lua
string moonCode = @"
class Animal
  new: (@name) =>
  
  speak: =>
    print 'Hello, my name is ' .. @name

dog = Animal 'Buddy'
dog\speak!
";

string luaCode = MoonScriptCompiler.Compile(moonCode);

// Теперь можно выполнить скомпилированный Lua код
var engine = new LuaEngine();
engine.Execute(luaCode);

Синтаксис MoonScript

Классы

class Player
  new: (@name, @health = 100) =>
    
  attack: (target) =>
    print @name .. " attacks " .. target
    
  heal: (amount) =>
    @health += amount
    print @name .. " healed for " .. amount

-- Использование
player = Player "Hero"
player\attack "Dragon"
player\heal 50

Скомпилированный Lua код:

local Player
do
  local _class_0
  local _base_0 = {
    attack = function(self, target)
      return print(self.name .. " attacks " .. target)
    end,
    heal = function(self, amount)
      self.health = self.health + amount
      return print(self.name .. " healed for " .. tostring(amount))
    end
  }
  _base_0.__index = _base_0
  _class_0 = setmetatable({
    __init = function(self, name, health)
      self.name = name
      if health == nil then
        health = 100
      end
      self.health = health
    end
  }, {
    __index = _base_0,
    __call = function(cls, ...)
      local _self_0 = setmetatable({}, _base_0)
      cls.__init(_self_0, ...)
      return _self_0
    end
  })
  Player = _class_0
end

Наследование

class Animal
  new: (@name) =>
  
  speak: =>
    print @name .. " makes a sound"

class Dog extends Animal
  speak: =>
    print @name .. " barks!"

class Cat extends Animal
  speak: =>
    print @name .. " meows!"

dog = Dog "Buddy"
cat = Cat "Whiskers"
dog\speak!  -- "Buddy barks!"
cat\speak!  -- "Whiskers meows!"

Списковые включения

-- Простое включение
squares = [x * x for x in *{1, 2, 3, 4, 5}]
-- результат: {1, 4, 9, 16, 25}

-- С условием
evens = [x for x in *{1, 2, 3, 4, 5, 6} when x % 2 == 0]
-- результат: {2, 4, 6}

-- Вложенные включения
pairs = [{x, y} for x in *{1, 2, 3} for y in *{4, 5, 6}]
-- результат: {{1,4}, {1,5}, {1,6}, {2,4}, ...}

-- Включения для таблиц
user_map = {user.id, user.name for user in *users}
-- результат: {[1]="Alice", [2]="Bob", ...}

Условные выражения

-- Тернарный оператор
status = if health > 0 then "alive" else "dead"

-- Многострочные условия
message = if score > 90
  "Отлично!"
elseif score > 70
  "Хорошо!"
elseif score > 50
  "Удовлетворительно"
else
  "Плохо"

-- Switch выражение
result = switch value
  when 1 then "one"
  when 2 then "two"
  when 3 then "three"
  else "other"

-- Unless (противоположность if)
print "Game Over" unless player.alive

-- Постфиксные условия
save_game! if auto_save_enabled
reload_level! unless level_completed

Деструктуризация

-- Таблицы
{x, y, z} = getPosition!

-- С остальными элементами
{first, second, ...rest} = {1, 2, 3, 4, 5}
-- first=1, second=2, rest={3, 4, 5}

-- Функции возвращающие несколько значений
{status, message} = validateInput data

-- С значениями по умолчанию
{name = "Unknown", age = 0} = getUserData!

-- Вложенная деструктуризация
{player: {x, y}, score} = getGameState!

Функции

-- Стрелочные функции
double = (x) -> x * 2
add = (a, b) -> a + b

-- Функции с жирной стрелкой (сохраняют self)
class Button
  onClick: (callback) =>
    @callback = callback
    
  trigger: =>
    @callback! if @callback

-- Значения по умолчанию
greet = (name = "Guest") -> print "Hello, " .. name

-- Variadic функции
sum = (...) ->
  total = 0
  for v in *{...}
    total += v
  total

-- Именованные параметры через таблицу
createWindow = ({width = 800, height = 600, title = "Window"}) ->
  print "Creating #{width}x#{height} window: #{title}"

Интерполяция строк

-- Двойные кавычки поддерживают интерполяцию
name = "World"
message = "Hello, #{name}!"

-- Можно вставлять выражения
x, y = 10, 20
coords = "Position: (#{x}, #{y})"

-- Вызов функций
player = {name: "Hero", level: 10}
info = "#{player.name} is level #{player.level}"

Операторы

-- Составные операторы
x += 5    -- x = x + 5
y -= 3    -- y = y - 3
z *= 2    -- z = z * 2
w /= 4    -- w = w / 4
s ..= "!" -- s = s .. "!"

-- Логические операторы
result = true and false  -- false
result = true or false   -- true
result = not true        -- false

-- С присваиванием
x = x or 10     -- можно написать как:
x or= 10

-- Проверка на nil
value = getValue!
value or= "default"

Циклы

-- For по числам
for i = 1, 10
  print i

-- For по таблице (массив)
for item in *items
  print item

-- For по таблице (ключ-значение)
for key, value in pairs table
  print key, value

-- While
while player.alive
  update!

-- Until (противоположность while)
until game_over
  play_frame!

-- Постфиксные циклы
print item for item in *items

With

-- Упрощает доступ к полям объекта
with player
  .x = 100
  .y = 200
  .health = 100
  \update!

-- Эквивалентно:
player.x = 100
player.y = 200
player.health = 100
player:update()

-- С возвращаемым значением
result = with create_object!
  .name = "MyObject"
  .value = 42

Интеграция в движок

Методы расширения

public static class LuaEngineExtensions
{
    /// <summary>
    /// Выполняет MoonScript код, предварительно скомпилировав его в Lua.
    /// </summary>
    public static object[] ExecuteMoonScript(this LuaEngine engine, string moonCode)
    {
        string luaCode = MoonScriptCompiler.Compile(moonCode);
        return engine.Execute(luaCode);
    }
    
    /// <summary>
    /// Компилирует MoonScript в Lua и создаёт скомпилированный скрипт.
    /// </summary>
    public static LuaScript CompileMoonScript(this LuaEngine engine, string moonCode)
    {
        string luaCode = MoonScriptCompiler.Compile(moonCode);
        return engine.Compile(luaCode);
    }
    
    /// <summary>
    /// Оценивает MoonScript выражение и возвращает результат.
    /// </summary>
    public static object[] EvaluateMoonScript(this LuaEngine engine, string moonCode)
    {
        string luaCode = MoonScriptCompiler.Compile(moonCode);
        return engine.Evaluate(luaCode);
    }
}

Использование

var engine = new LuaEngine();

// Прямое выполнение
var result = engine.ExecuteMoonScript(@"
    add = (a, b) -> a + b
    return add 5, 3
");
Console.WriteLine(result[0]); // 8

// Компиляция для повторного использования
var script = engine.CompileMoonScript(@"
    multiply = (a, b) -> a * b
    return multiply x, y
");

script.SetValue("x", 10);
script.SetValue("y", 5);
var result = script.Evaluate();
Console.WriteLine(result[0]); // 50

Загрузка из файлов

// Загрузка MoonScript из файла
string moonCode = File.ReadAllText("script.moon");
string luaCode = MoonScriptCompiler.Compile(moonCode);

var engine = new LuaEngine();
engine.Execute(luaCode);

// Кеширование скомпилированного кода
var cache = new Dictionary<string, string>();

string GetCompiledScript(string path)
{
    if (!cache.TryGetValue(path, out string luaCode))
    {
        string moonCode = File.ReadAllText(path);
        luaCode = MoonScriptCompiler.Compile(moonCode);
        cache[path] = luaCode;
    }
    return luaCode;
}

Обработка ошибок

Ошибки компиляции

using IgdrasilEngine.Engine.Scripting.Exceptions;

try
{
    string moonCode = @"
        class Invalid
          bad syntax here
    ";
    
    string luaCode = MoonScriptCompiler.Compile(moonCode);
}
catch (NLua.Exceptions.LuaException ex)
{
    Console.WriteLine($"Ошибка компиляции MoonScript: {ex.Message}");
    // Пример вывода:
    // Parse error: [string "compiler"]:4: unexpected symbol near 'bad'
}

Ошибки выполнения

try
{
    var engine = new LuaEngine();
    string luaCode = MoonScriptCompiler.Compile(@"
        divide = (a, b) -> a / b
        return divide 10, 0
    ");
    
    var result = engine.Execute(luaCode);
}
catch (ScriptExecutionException ex)
{
    Console.WriteLine($"Ошибка выполнения: {ex.Message}");
}

Отладка

Поскольку MoonScript компилируется в Lua, ошибки времени выполнения будут ссылаться на строки Lua кода:

// Сохраняем оба варианта для отладки
string moonCode = File.ReadAllText("game.moon");
string luaCode = MoonScriptCompiler.Compile(moonCode);

// Сохраняем скомпилированный код для отладки
File.WriteAllText("game.lua", luaCode);

try
{
    engine.Execute(luaCode);
}
catch (Exception ex)
{
    Console.WriteLine($"Ошибка в game.lua: {ex.Message}");
    Console.WriteLine("Проверьте game.lua для отладки");
}

Практические примеры

Игровая логика

-- Система врагов
class Enemy
  new: (@name, @health, @damage) =>
    @alive = true
  
  takeDamage: (amount) =>
    @health -= amount
    if @health <= 0
      @onDeath!
      
  onDeath: =>
    @alive = false
    print "#{@name} has been defeated!"
    
  attack: (target) =>
    if @alive
      target\takeDamage @damage
      print "#{@name} attacks for #{@damage} damage!"

-- Создание волны врагов
spawnWave = (count, prefix) ->
  [Enemy "#{prefix} #{i}", 50 + i * 10, 10 for i in *[1..count]]

-- Использование
enemies = spawnWave 5, "Goblin"
for enemy in *enemies when enemy.alive
  enemy\attack player

Система инвентаря

class Item
  new: (@name, @value, @weight) =>

class Inventory
  new: (@capacity = 20) =>
    @items = {}
    @weight = 0
  
  canAdd: (item) =>
    @weight + item.weight <= @capacity
  
  add: (item) =>
    if @canAdd item
      table.insert @items, item
      @weight += item.weight
      true
    else
      false
  
  remove: (item) =>
    for i, inv_item in ipairs @items
      if inv_item == item
        table.remove @items, i
        @weight -= item.weight
        return true
    false
  
  getTotalValue: =>
    sum = 0
    for item in *@items
      sum += item.value
    sum

-- Использование
inventory = Inventory 50
inventory\add Item "Sword", 100, 5
inventory\add Item "Shield", 80, 10
print "Total value: #{inventory\getTotalValue!}"

Конечный автомат (State Machine)

class StateMachine
  new: (@initial_state) =>
    @current_state = @initial_state
    @states = {}
  
  addState: (name, state) =>
    @states[name] = state
  
  transition: (new_state_name) =>
    new_state = @states[new_state_name]
    return unless new_state
    
    @current_state\exit! if @current_state.exit
    @current_state = new_state
    @current_state\enter! if @current_state.enter
  
  update: (dt) =>
    @current_state\update dt if @current_state.update

-- Состояния игрока
idle_state = {
  enter: -> print "Entering idle"
  update: (dt) -> -- обновление
  exit: -> print "Exiting idle"
}

walk_state = {
  enter: -> print "Start walking"
  update: (dt) -> -- обновление ходьбы
  exit: -> print "Stop walking"
}

-- Использование
fsm = StateMachine idle_state
fsm\addState "idle", idle_state
fsm\addState "walk", walk_state
fsm\transition "walk"

Обработка данных

-- Парсинг и фильтрация конфигурации
parseConfig = (raw_data) ->
  lines = [line for line in raw_data\gmatch "[^\n]+"]
  config = {}
  
  for line in *lines
    continue if line\match "^%s*#"  -- пропуск комментариев
    continue if line\match "^%s*$"  -- пропуск пустых строк
    
    key, value = line\match "(%S+)%s*=%s*(.+)"
    if key and value
      config[key] = value
  
  config

-- Агрегация статистики
calculateStats = (players) ->
  total_score = 0
  max_score = 0
  min_score = math.huge
  
  for player in *players
    total_score += player.score
    max_score = math.max max_score, player.score
    min_score = math.min min_score, player.score
  
  {
    average: total_score / #players
    max: max_score
    min: min_score
    total: total_score
  }

Преимущества MoonScript

1. Читаемость

Lua:

local function calculate(x, y, z)
  local result = x * y
  if result > z then
    return result - z
  else
    return result + z
  end
end

MoonScript:

calculate = (x, y, z) ->
  result = x * y
  if result > z then result - z else result + z

2. ООП

Lua (требует метатаблицы):

local Player = {}
Player.__index = Player

function Player.new(name, health)
  local self = setmetatable({}, Player)
  self.name = name
  self.health = health
  return self
end

function Player:heal(amount)
  self.health = self.health + amount
end

MoonScript:

class Player
  new: (@name, @health) =>
  
  heal: (amount) =>
    @health += amount

3. Списковые включения

Lua:

local squares = {}
for i = 1, 10 do
  if i % 2 == 0 then
    table.insert(squares, i * i)
  end
end

MoonScript:

squares = [i * i for i in *[1..10] when i % 2 == 0]

4. Безопасность

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

Lua (ошибка - глобальная переменная):

function calculate()
  result = 42  -- глобальная!
  return result
end

MoonScript (автоматически локальная):

calculate = ->
  result = 42  -- локальная
  result

Ограничения

1. Время компиляции

Компиляция MoonScript добавляет накладные расходы:

var sw = Stopwatch.StartNew();
string luaCode = MoonScriptCompiler.Compile(moonCode);
sw.Stop();
Console.WriteLine($"Compilation took: {sw.ElapsedMilliseconds}ms");

Рекомендация: Компилируйте заранее для продакшн сборок.

2. Отладка

Ошибки ссылаются на строки Lua кода:

-- script.moon, строка 5
class Player
  attack: =>
    @health += 10  -- ошибка: должно быть -=

Ошибка покажет строку в Lua коде, не в MoonScript.

Решение: Сохраняйте скомпилированный Lua код для отладки.

3. Размер сборки

Компилятор MoonScript встроен в сборку, увеличивая её размер (~200KB).

4. Совместимость

Не весь Lua код можно написать в MoonScript идиоматично. Некоторые низкоуровневые операции проще писать на Lua.

Рекомендации

1. Используйте для сложной логики

MoonScript отлично подходит для:

2. Кешируйте результаты

public class MoonScriptCache
{
    private readonly Dictionary<string, string> _cache = new();
    
    public string GetCompiled(string moonCode)
    {
        string hash = ComputeHash(moonCode);
        
        if (!_cache.TryGetValue(hash, out string luaCode))
        {
            luaCode = MoonScriptCompiler.Compile(moonCode);
            _cache[hash] = luaCode;
        }
        
        return luaCode;
    }
    
    private string ComputeHash(string input)
    {
        using var sha = SHA256.Create();
        var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input));
        return Convert.ToBase64String(bytes);
    }
}

3. Разделяйте разработку и продакшн

Development:

// Компиляция на лету для быстрой итерации
string moonCode = File.ReadAllText("script.moon");
string luaCode = MoonScriptCompiler.Compile(moonCode);
engine.Execute(luaCode);

Production:

// Предварительная компиляция
// Build script:
// moonc -t lua_scripts script.moon

string luaCode = File.ReadAllText("script.lua");
engine.Execute(luaCode);

4. Документируйте

Используйте комментарии для облегчения отладки:

-- Система здоровья игрока
-- @param amount количество здоровья для восстановления
-- @return новое значение здоровья
heal = (amount) ->
  @health = math.min @health + amount, @max_health
  print "Healed for #{amount}, current health: #{@health}"
  @health

5. Тестируйте

[Test]
public void TestMoonScriptCompilation()
{
    string moonCode = @"
        add = (a, b) -> a + b
        return add 5, 3
    ";
    
    string luaCode = MoonScriptCompiler.Compile(moonCode);
    Assert.IsNotNull(luaCode);
    
    var engine = new LuaEngine();
    var result = engine.Execute(luaCode);
    Assert.AreEqual(8.0, result[0]);
}

Производительность

Сравнение времени компиляции

Размер скрипта Время компиляции
Маленький (< 100 строк) < 10ms
Средний (100-500 строк) 10-50ms
Большой (500-1000 строк) 50-150ms
Очень большой (> 1000 строк) > 150ms

Оптимизация

// Плохо: компиляция в каждом кадре
void Update()
{
    string luaCode = MoonScriptCompiler.Compile(moonScript);
    engine.Execute(luaCode);
}

// Хорошо: компиляция один раз
private LuaScript _compiledScript;

void Start()
{
    string luaCode = MoonScriptCompiler.Compile(moonScript);
    _compiledScript = engine.Compile(luaCode);
}

void Update()
{
    _compiledScript.Evaluate();
}

Ресурсы

Официальная документация

Примеры

Сообщество