Aplikacje typu REPL (Read, Execute, Print, Loop) są wykorzystywane w celu udostępnienia szybkiego środowiska uruchomieniowego dla kodu pisanego w języku X. Przykładami takich „konsol” jest chociażby konsola F# interactive, czy także konsole IPY i RP udostępnione przez IronPython i IronRuby.

W aplikacjach typu REPL chodzi o to, że input wprowadzony przez użytkownika (READ) jest od razu przetwarzany i wykonywany (EXECUTE) zwracając wynik użytkownikowi (PRINT) dając mu możliwość wprowadzenia następnego polecenia (LOOP). Dzięki czemu programiści nie muszą zaciągać wielkich systemów typu VS w celu sprawdzenia czy ich kod działa, albo co robi funkcjaputs z Ruby.

Do tej pory, tworzenie REPL było dość skompilowanym zadaniem, choć na pewno nie, niewykonywanym, przykładem może być chociażby REPL dla języka C# napisany przez Dona Boxa,a następnie rozwinięty przez Ronalda Matta do obsługi wielolinijkowego kodu. Najnowszy REPL dla C# jest dostępny w Mono. Ale jak widać nie są to aplikacje 2-3 linijkowe tylko przeważnie zajmują dość sporo kodu i też nie każda funkcja jest dostępna.

Wraz z nadchodzącym .NET Framework oraz z zintegrowanym z nim DLR (Dynamic Language Runtime, aktualnie dostępnym na CodePlex), nie tylko tworzenie języków na platformę .NET zostanie ułatwione, ale także tworzenie do nich aplikacji typu REPL.

Już od dawna w sieci można znaleźć przykłady jak wykorzystać DLR w celu hostowania języka IronPyton w C# – przykład nawet był podany na CodeCamp Warszawa 2009. Jednakże nie znalazłem jeszcze przykładu, który umożliwiłby rozsądne hostowanie języka ze wsparciem wielolinijkowym. Pod rozsądnym rozumiem to iż nie musimy wciskać specjalnej kombinacji klawiszy w celu wykonania wprowadzonego kodu, ale w zależności od tego co wprowadziliśmy do REPL kod albo się wykona albo poczeka na dalszy INPUT.

Niestety większość przykładów w sieci podaje sposób utworzenia środowiska uruchomieniowego dla języka X za pomocą utworzenia jego silnika (engine) a następnie wykonanie metody Executena przekazanym ciągu znaków. Oczywiście 3/4 tych przykładów wykorzystuje kod, który już nie istnieje w DLR, i tak naprawdę można o nich zapomnieć ;) Jednak można je sprawdzić do kodu poniżej:

/// <summary>
/// Hosts dynamic engine for specified language.
/// </summary>
public class IronEngine
{
    /// <summary>
    /// Dynamic Language Runtime.
    /// </summary>
    public static readonly ScriptRuntime Runtime = ScriptRuntime.CreateFromConfiguration();
 
    /// <summary>
    /// Gets or sets the language hosting API.
    /// </summary>
    /// <value>The language hosting API.</value>
    public ScriptEngine Engine { get; set; }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="IronEngine"/> class and
    /// creates <see cref="ScriptEngine"/> of specified <paramref name="name"/>.
    /// </summary>
    /// <param name="name">The name of the language hosting API.</param>
    public IronEngine(string name)
    {
        this.Engine = Runtime.GetEngine(name);
    }
 
    /// <summary>
    /// Executes the line of code.
    /// </summary>
    /// <param name="code">The line of code.</param>
    /// <returns>
    /// Execution result.
    /// </returns>
    public object ExecuteLine(string code)
    {
        return this.Engine.Execute(code);
    }
}

Jak widać nie jest to trude, to co najważniejsze to na początku pobieramy konfigurację DLR z App/Web.config za pomocą metody:

ScriptRuntime.CreateFromConfiguration();

Teraz mamy już dostęp do wszystkich dynamicznych języków, jakie zostały ustalone w konfiguracji:

<configuration>
  <configSections>
    <section name="microsoft.scripting" type="Microsoft.Scripting.Hosting.Configuration.Section, Microsoft.Scripting, Version=0.9.6.10, Culture=neutral, PublicKeyToken=null" requirePermission="false" />
  </configSections>
 
  <microsoft.scripting>
    <languages>
      <language names="IronPython;Python;py" extensions=".py" displayName="IronPython 2.6 Alpha" type="IronPython.Runtime.PythonContext, IronPython, Version=2.6.0.10, Culture=neutral, PublicKeyToken=null" />
      <language names="IronRuby;Ruby;rb" extensions=".rb" displayName="IronRuby 0.4" type="IronRuby.Runtime.RubyContext, IronRuby, Version=0.4.0.0, Culture=neutral, PublicKeyToken=null" />
    </languages>
 
    <options>
      <set language="Ruby" option="LibraryPaths" value="....LanguagesRubylibs;......External.LCA_RESTRICTEDLanguagesRubyredist-libsrubysite_ruby1.8;......External.LCA_RESTRICTEDLanguagesRubyredist-libsruby1.8" />
    </options>
  </microsoft.scripting>
</configuration>

Czyli w przypadku powyżej mamy dostęp do języka IronPython i IronRuby. Wykorzystanie takiego przykładowego REPL jest bardzo proste, wystarczy, że zrobimy coś takiego:

// creating ruby engine
IronEngine ruby = new IronEngine("rb");
// creating python engine
IronEngine python = new IronEngine("py");
 
// testing Add method from created engines
Console.WriteLine("IronRuby Code Add Test:tt{0}", ruby.ExecuteLine(string.Format("{0} + {1}", x, y)));
Console.WriteLine("IronPython Code Add Test:t{0}", python.ExecuteLine(string.Format("{0} + {1}", x, y)));

Proste i przyjemne ;) Można to obrać w while jak i innego rodzaju pętle i mamy jednolinijkowy REPL zaimplementowany. Niestety, nie da to nam możliwości utworzenia klas jak i przetestowania bardziej zaawansowanych funkcji.

Naszym celem będzie napisanie aplikacji konsolowej, tak by można było wykonywać kilka linii kodu z wybranego i z skonfigurowanego w app.config języka – czyli stworzymy sobie multilanguage REPL :)

Zanim przejdziemy do implementacji, proponuje ściągnąć najnowszą wersję DLR z CodePlex (albo źródła, albo dllki). Jest to dość ważne ;) przez DLL od języków dynamicznych jak i od DLR, nie uda nam się wykonać prawie ani jednej linii kodu z przykładu który zaraz napiszemy ;)

No dobrze, środowiska gotowe, więc tworzymy projekt C# Console Application, i dodajemy referencje do następujących bibliotek:

  • IronPython – biblioteka dla języka IronPython;
  • IronPython.Modules – modułu z których IronPython korzysta;
  • IronRuby – biblioteka języka IronRuby;
  • IronRuby.Libraries – zbiór bibliotek z których IronRuby korzysta;
  • Microsoft.Scripting – udostępnia metody do obsługi języków dynamicznych;
  • Microsoft.Scripting.Core – udostępnia kompilator oraz drzewo AST;
  • Microsoft.Scripting.ExtensionAttribute – roszerzenia.

Tworzymy także plik App.config i kopiujemy do niego konfigurację z pobranego DLR (tak będzie łatwiej a plik jest na tyle prosty, że tłumaczyć go chyba nie trzeba).

Mamy już wszystko tak naprawdę przygotowane, jedyne co nam zostało to napisanie kodu :)

/// <summary>
/// Language enumeration.
/// </summary>
public enum Language
{
    /// <summary>
    /// Python.
    /// </summary>
    IronPython,
    /// <summary>
    /// Ruby.
    /// </summary>
    IronRuby
}
 
/// <summary>
/// Read, Execute, Print, Loop Sample.
/// </summary>
public class Repl
{
    /// <summary>
    /// Dynamic Language Runtime.
    /// </summary>
    public static readonly ScriptRuntime Runtime = ScriptRuntime.CreateFromConfiguration();
 
    /// <summary>
    /// Language hosting API.
    /// </summary>
    private ScriptEngine _engine;
    /// <summary>
    /// Scope of the code that is going to be executed.
    /// </summary>
    private ScriptScope _scriptScope;
    /// <summary>
    /// Provides exceptions that occures in code execution.
    /// </summary>
    private ExceptionOperations _exceptionOperations;
    /// <summary>
    /// Gets or sets current language string.
    /// </summary>
    /// <value>The current language string.</value>
    public string LanguageName { get; set; }
    /// <summary>
    /// Gets or sets the current language.
    /// </summary>
    /// <value>The current language.</value>
    public Language CurrentLanguage { get; set; }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="Repl"/> class and
    /// creates IronPython Engine.
    /// </summary>
    public Repl()
    {
        this.LanguageName = "py";
        this._engine = Repl.Runtime.GetEngine(this.LanguageName);
        this._scriptScope = this._engine.CreateScope();
        this._exceptionOperations = this._engine.GetService<ExceptionOperations>();
    }
 
    /// <summary>
    /// Sets the color of the language (the first 3 characters).
    /// </summary>
    private void SetLanguageColor()
    {
        switch (this.CurrentLanguage)
        {
            case Language.IronPython:
                Console.ForegroundColor = ConsoleColor.Yellow;
                break;
            case Language.IronRuby:
            default:
                Console.ForegroundColor = ConsoleColor.White;
                break;
        }
    }
 
    /// <summary>
    /// Prints the language line in selected color.
    /// </summary>
    /// <remarks>
    /// New language line occures ("{0}> ") when the previous code has been executed
    /// or language of the repl has been changed.
    /// </remarks>
    private void PrintLanguageLineNew()
    {
        this.SetLanguageColor();
        Console.Write("{0}> ", this.LanguageName);
        Console.ResetColor();
    }
 
    /// <summary>
    /// Prints the next language line in selected color.
    /// </summary>
    /// <remarks>
    /// Next language line occures ("{0}| ") when the fisrt line is a part
    /// of the complex code (i.e: class definition):
    /// Sample in IronRuby:
    /// <code>
    /// rb> class Calc
    /// rb|     def add(x, y)
    /// rb|         x + y
    /// rb|     end
    /// rb| end
    /// </code>
    /// </remarks>
    private void PrintLanguageLineNext()
    {
        this.SetLanguageColor();
        Console.Write("{0}| ", this.LanguageName);
        Console.ResetColor();
    }
 
    /// <summary>
    /// Execute Repl.
    /// </summary>
    public void Run()
    {
        // while not exit
        while(true)
        {
            // prints language line "py> ", "rb> "
            this.PrintLanguageLineNew();
 
            // read user input
            string line = Console.ReadLine();
 
            // if input is null or empty, replay print line and read
            if(string.IsNullOrEmpty(line))
            {
                continue;
            }
 
            // if line starts with # execute admin command
            if(line[0] == '#')
            {
                // execute admin command
                this.ExecuteCommand(line.Substring(1));
            }
            else
            {
                // execute code
                this.ExecuteCode(this.ReadCode(line));
            }
        }
    }
 
    /// <summary>
    /// Reads the line of code and decide what to do.
    /// </summary>
    /// <param name="line">The line of code.</param>
    /// <returns>
    /// ScriptSource representation of the source code.
    /// </returns>
    public ScriptSource ReadCode(string line)
    {
        // keeps the code that will need to be executed
        StringBuilder code = new StringBuilder();
        // and adds passed line to it.
        code.AppendLine(line);
 
        // while code still needs input from the user
        // continue to ask user for the code.
        while(true)
        {
            // represents a source code and offers few ways to execute it
            ScriptSource scriptSource = this._engine.CreateScriptSourceFromString(code.ToString(),
                                                                                  SourceCodeKind.InteractiveCode);
 
            // if code is completed or is invalid return the source
            if(scriptSource.GetCodeProperties() == ScriptCodeParseResult.Complete
                || scriptSource.GetCodeProperties() == ScriptCodeParseResult.Invalid)
            {
                return scriptSource;
            }
            else
            {
                // otherwise ask for the next line
                this.PrintLanguageLineNext();
 
                line = Console.ReadLine();
 
                // and if this line is empty return the source
                if (string.IsNullOrEmpty(line))
                    return scriptSource;
 
                // or append it and repeat actions
                code.AppendLine(line);
            }
        }
    }
 
    /// <summary>
    /// Executes the the passed source code.
    /// </summary>
    /// <param name="scriptSource">The source code to be executed.</param>
    public void ExecuteCode(ScriptSource scriptSource)
    {
        try
        {
            // execute passed code
            scriptSource.Execute(this._scriptScope);
        }
        catch(Exception ex)
        {
            string message;
            string name;
            // read the exception from the execution
            this._exceptionOperations.GetExceptionMessage(ex, out message, out name);
 
            // set console colors and prints the error.
            Console.BackgroundColor = ConsoleColor.Red;
            Console.ForegroundColor = ConsoleColor.Black;
            Console.WriteLine("{0}: {1}", name, message);
            Console.ResetColor();
        }
    }
 
    /// <summary>
    /// Executes the admin command.
    /// </summary>
    /// <param name="command">The admin command.</param>
    public void ExecuteCommand(string command)
    {
        switch(command)
        {
            case "exit":
                Environment.Exit(0);
                break;
            case "list":
                this.DisplayLanguages();
                break;
            default:
                if (!this.SwitchLanguage(command))
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine("Unknown command '{0}'", command);
                    Console.ResetColor();
                }
                break;
        }
    }
 
    /// <summary>
    /// Switches the language.
    /// </summary>
    /// <param name="name">The language name.</param>
    /// <returns>
    ///     <c>true</c> if swithc successed; otherwise <c>false</c>.
    /// </returns>
    private bool SwitchLanguage(string name)
    {
        ScriptEngine engine;
        if(this._engine.Runtime.TryGetEngine(name, out engine))
        {
            this.LanguageName = name;
            this._engine = engine;
            if (name == "py" || name == "IronPython")
                this.CurrentLanguage = Language.IronPython;
            else
                this.CurrentLanguage = Language.IronRuby;
 
            return true;
        }
 
        return false;
    }
 
    /// <summary>
    /// Displays the languages.
    /// </summary>
    private void DisplayLanguages()
    {
        foreach(var language in Repl.Runtime.Setup.LanguageSetups)
        {
            Console.WriteLine("{0}: {1}", language.DisplayName, string.Join(", ", language.Names.ToArray()));
        }
    }

Niby go dużo, ale głównie przez komentarze. To co jest dla nas najważniejsze to zmienne:

/// <summary>
/// Language hosting API.
/// </summary>
private ScriptEngine _engine;
/// <summary>
/// Scope of the code that is going to be executed.
/// </summary>
private ScriptScope _scriptScope;
/// <summary>
/// Provides exceptions that occures in code execution.
/// </summary>
private ExceptionOperations _exceptionOperations;

Które udostępnią nam silnik języka dynamicznego, pozwolą na wykonanie kodu w określonym przez nas zakresie oraz dadzą nam wyniki błędnego wykonania. Także te dwie linijki są bardzo ważne:

// represents a source code and offers few ways to execute it
ScriptSource scriptSource = this._engine.CreateScriptSourceFromString(code.ToString(),
                                                                      SourceCodeKind.InteractiveCode);
 
// if code is completed or is invalid return the source
if(scriptSource.GetCodeProperties() == ScriptCodeParseResult.Complete
    || scriptSource.GetCodeProperties() == ScriptCodeParseResult.Invalid)
{
    return scriptSource;
}

Dzięki nim wiemy kiedy mamy wykonać kod, który wykonuje się bardzo prosto:

// execute code in our scope

scriptSource.Execute(this._scriptScope);

Reszta to jest po prostu „dodatek” by wszystko ładnie wyglądało i dało się z tego korzystać. Także zostały zaimplementowane trzy polecenia administracyjne:

  1. #exit – wychodzi z aplikacji;
  2. #list – wyświetla listę dostępnych języków;
  3. #LANGUAGE – zmienia język na wybrany.

Korzystanie z naszego REPL też jest dość proste:

Repl repl = new Repl();

repl.Run();

W porównaniu do tego, co trzeba było zrobić w C#… to pisanie kodu z wykorzystaniem DLR to czysta przyjemność :)

Powyższy przykład jedynie przedstawia najprostszy wariant REPL, możemy pomyśleć o niebo lepszym napisanym w WPF i udostępniającym kilka języków na raz (za pomocą okien). Możemy także przypatrzeć się implementacji REPL przez IronPython/IronRuby i wykorzystać te same klasy w celu zwiększenia kontrolki nad naszym REPL. Możliwości jest wiele, ja zaprezentowałem jedną z nich :)

Mam nadzieję że znajdę trochę czasu jeszcze by pokazać tak naprawdę co i jak można zrobić za pomocą DLR bo zrobić będzie można naprawdę bardzo dużo :)