Re[2]: AST-based solution
От: fionbio  
Дата: 14.07.05 10:37
Оценка: 54 (9)
[ Эх, обогнали меня Вчера накалякал пример, хоть и башка
была весь день какая-то мутная — странно, не пил вроде. Запостить
вчера моральных сил не хватило ]

Попробую привести решение в духе Лиспа

Если я правильно понимаю, то для публики может представлять
интерес несколько более общее решение задачи. В связи с этим
я хотел бы привести пример с использованием AST. Преимущества
такого подхода, думаю, известны — на основе этой инфраструктуры
можно легко создавать другие, более сложные и полезные генераторы
кода (см R#). Кроме того, мы фактически можем применять подобие
lisp macros к C#.

Если коллег интересует более компактное решение, я могу привести
и сокращённый вариант без деревьев. Также могу сказать, что для Лиспа
есть готовые либы для работы с text templates, например, CL-EMB
(в добавок я ещё свою достаточно удобную примочку как-то написал
для темплейтенья сырцов, я её использовал для VB.NET).

Я буду использовать стандартный Common Lisp Pretty Printer.
Это cразу исключает CLisp, т.к. AFAIK в нём эта часть не доделана.
Код работает с SBCL, Allegro, LispWorks; должен работать с CMUCL.
Данный pretty printer представляет из себя специфическое чудо техники
и конфигурируем в такой степени, что в идеале можно на входе давать
Lisp (списочные структуры), а на выходе получать C#. В силу природной
лени (и, возможно, опыта программирования на Perl ) я воспользовался
в паре мест не особо читабельными строками формата, за что прошу меня
извинить (можно было бы обойтись и без них).

Сразу забегу вперёд и приведу ссылку на решение. Оно может
показаться длинным, на самом деле кода там 70 строк, не
считая данных-примеров, докстрингов функций и комментариев.
Объём, конечно, чудовищный, ничего не скажешь.

Обработанный colorize'ом вариант — тут:
http://depni.sinp.msu.ru/~ivan_iv/lisp/sharpclass.html

Сам исходник — тут:
http://depni.sinp.msu.ru/~ivan_iv/lisp/sharpclass.lisp

Для пущей интересности задачу немного расширим — пусть
для пропертей генерятся не только getter'ы, но и setter'ы,
плюс генерится конструктор без аргументов (в добавок к
конструктору с аргументами) и все классы заворачиваются
в общий namespace

В качестве исходных данных вместо

<objects>
    <object name="Obj1">
        <property name="prop1" type="int"/>
        <property name="prop2" type="string"/>
    </object>
</objects>


мы будем использовать

(namespace "MyNS"
 (object "Obj1"
  (property "prop1" "int")
  (property "prop2" "string")
  (property "dblprop" "double"))
 (object "Obj2"
  (property "someprop" "object[]")))


Следует учесть, что символы namespace, object и property
данным генератором не используются и добавлены здесь для
читабельности.

Вводим промежуточное представление — AST.
Идея заключается в следующем — у нас есть дерево, в
виде символьного выражения записываемое, например, так:
(:namespace "MyTestNS"
 (:class "MyTestClass"
  (:field "int" "_someprop")
  (:constructor "MyTestClass" ((:arg "int" "someprop"))
   (:comment "This is a test")
   (:setf (:id "_someprop") (:id "someprop")))
  (:property "int" "SomeProp"
   (:get (:return (:id "_someprop")))
   (:set (:setf (:id "_someprop") (:id "value"))))
  (:property "int" "AnotherProp"
   (:get (:return 0)))))


В виде XML его можно было бы записать, например, следующим образом:
<namespace name="MyTestNS">
  <class name="MyTestClass">
    <field name="_someprop" type="int"/>
    <constructor name="MyTestClass">
      <args>
        <arg name="someprop" type="int"/>
      </args>
      <body>
        <set>
          <lvalue>
            <identifier name="_someprop">
          </lvalue>
          <rvalue>
            <identifier name="someprop">
          </rvalue>
        </set>
      </body>
    </constructor>
    <property name="SomeProp" type="int">
      ...
    </property>
    ...
</namespace>


Данный AST является достаточно упрощённым, но для наших целей этого
хватит.

Далее, нам надо настроить pretty-printer. Мы определяем способы печати
различных узлов дерева (data-driven approach). Для этого нам нужен следующий framework:

;;; AST printer, much more concise than your CodeDOM and perhaps R# :-P
;;; Can be easily extended to support other language constructs &
;;; several languages simultaneously

;; pretty-print dispatch table: type -> printing func correspondence
(defvar *dispatch* (copy-pprint-dispatch))

(defun ast-print (x &optional (stream *standard-output*))
  "Print the expression x using AST dispatch rules"
  (write x :pretty t :pprint-dispatch *dispatch* :stream stream))

(defun ast-print-form (stream form)
  "Print the AST node (form)"
  (if (and (symbolp (first form))
           (get (first form) 'ast-form))
      (funcall (get (first form) 'ast-form) stream form)
      (error "unknown form: ~S" form)))

;; set dispatch function for cons type in our pretty-print dispatch table
(set-pprint-dispatch 'cons #'ast-print-form 0 *dispatch*)

(defmacro defprinter (name args &body body)
  "Define a printer for specified AST node type"
  (let ((func-name (intern (format nil "PRINT-FORM-~A" name)))
        (item (gensym)))
    `(progn
       (defun ,func-name (stream ,item)
         (destructuring-bind ,args (rest ,item)
           ,@body))
       (setf (get ',name 'ast-form) ',func-name))))


Функция (ast-print ...) печатает AST с использованием созданной
конфигурации. Макрос defprinter позволяет задавать обработчики
узлов дерева с указанием структуры. Функция ast-print-form является
обработчиком, печатающим значения типа cons — он, собственно,
вызывает нужный хендлер.

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

;; определяем простую функцию для печати - чтобы возвращаемое
;; значение не мешало обзору

CL-USER> (defun p (x) (ast-print x) nil)
P

;; играемся

CL-USER> (p '(:id "qwerty"))
qwerty
NIL
CL-USER> (p '(:return (:id "qwerty")))
return qwerty;
NIL
CL-USER> (p '(:property "int" "Qwerty" (:get (:return (:id "qwerty")))))
public int Qwerty
{
    get
    {
        return qwerty;
    }
}
NIL
CL-USER> (p '(:property "int" "Qwerty"
          (:get (:return (:id "qwerty")))
          (:set (:setf (:id "qwerty") (:id "value")))))
public int Qwerty
{
    get
    {
        return qwerty;
    }
    set
    {
        qwerty = value;
    }
}
NIL


Вроде как полезненно. Способствует. Посмотим, что внутрях.
Там есть одна утилитная функция с кривой строкой формата, которая
печатает ... { ... } с новыми строками и индентацией, плюс обработчики.

;;; AST printing specs for C#
;;; (write other specs, get VB.NET, Java, C++, Fortran, etc.)

;; please note that the following function could be writen in a more
;; clear way using pprint-* funcs, but now I'm just too lazy
(defun print-statement-with-body (stream body two-newlines fmt &rest args)
  "Print the statement with brace-enclosed body"
  (format stream
          (if two-newlines
              "~?~:@_~@<{~4i~{~:@_~w~^~:@_~}~i~:@_}~:>"
              "~?~:@_~@<{~4i~{~:@_~w~}~i~:@_}~:>")
          fmt args body))

;; identifier - printed literally
(defprinter :id (id)
  (write-string id stream))

;; private int x;
(defprinter :field (type name)
  (format stream "private ~a ~a;" type name))

;; public int SomeProp { ... }
(defprinter :property (type name &rest body)
  (print-statement-with-body stream body nil "public ~a ~a" type name))

;; get { ... }
(defprinter :get (&rest body)
  (print-statement-with-body stream body nil "get"))

;; set { ... }
(defprinter :set (&rest body)
  (print-statement-with-body stream body nil "set"))

;; return x;
(defprinter :return (expr)
  (format stream "return ~w;" expr))

;; argument
(defprinter :arg (type name)
  (format stream "~a ~a" type name))

;; constructor
(defprinter :constructor (name args &rest body)
  (print-statement-with-body stream body nil "~a(~@<~{~w~^, ~_~}~:>)" name args))

;; a = b;
(defprinter :setf (lvalue rvalue)
  (format stream "~w = ~w;" lvalue rvalue))

;; public class SomeClass { ... }
(defprinter :class (name &rest body)
  (print-statement-with-body stream body t "public class ~a" name))

;; namespace SomeNS { ... }
(defprinter :namespace (name &rest body)
  (print-statement-with-body stream body t "namespace ~a" name))

;; comment
(defprinter :comment (text)
  (format stream "// ~a" text))


Может быть, интересно взглянуть, во что разворачивается defprinter.
Ставим курсор в емаксе на последнюю закрывающуюся скобку и давим Ctrl-C Enter.

(PROGN (DEFUN PRINT-FORM-COMMENT (STREAM #:G288)
         (DESTRUCTURING-BIND (TEXT) (REST #:G288)
                             (FORMAT STREAM "// ~a" TEXT)))
       (SETF (GET ':COMMENT 'AST-FORM) 'PRINT-FORM-COMMENT))


Тут мы видим определение функции для разборки и печати узла
(комментария) и прицепление её к символу :comment (:comment, а не comment —
чтобы это был keyword, не привязанный к пакету — лисповому namespace'у)
через пропертю AST-FORM (здесь пропертя значит именованное значение,
привязанное к символу). #:G288 — сгенерированный (gensym) символ, гарантированно
не конфликтующий с другими именами из environment'а. Вместо
функции можно было бы использовать lambda, но это затруднило бы
отладку — функцию можно отTRACE'ить — после (trace print-form-comment)
в REPL будут выводиться все её вызовы с аргументами и возвращаемыми
значениями.

Следует отметить, что лисповские символы по умолчанию к регистру
не чувствительны (в связи с этим для простоты я решил использовать
строки вместо символов для названий C# пропертей и типов — хотя
проблема решается несложно). Большими буквами обычно выводится ответ Lisp'а.

Давайте ещё раз попробуем:
CL-USER> (p '(:namespace "MyTestNS"
          (:class "MyTestClass"
           (:field "int" "_someprop")
           (:constructor "MyTestClass" ((:arg "int" "someprop"))
        (:comment "This is a test")
        (:setf (:id "_someprop") (:id "someprop")))
           (:property "int" "SomeProp"
        (:get (:return (:id "_someprop")))
        (:set (:setf (:id "_someprop") (:id "value"))))
           (:property "int" "AnotherProp"
        (:get (:return 0))))))
namespace MyTestNS
{
    public class MyTestClass
    {
        private int _someprop;

        MyTestClass(int someprop)
        {
            // This is a test
            _someprop = someprop;
        }

        public int SomeProp
        {
            get
            {
                return _someprop;
            }
            set
            {
                _someprop = value;
            }
        }

        public int AnotherProp
        {
            get
            {
                return 0;
            }
        }
    }
}
NIL


Ну вот, теперь мы кажись могём из символьных выражений генерить C#.
И вроде как даже существенно удобоваримей того же CodeDOM всё выходит.
Ну, теперь дело за малым — делаем функцию-генератор AST на основе
декларативного описания:

;;; Data class generation

(defun generate-csharp-class (object)
  "Generate a C# class from DSL spec"
  (destructuring-bind (class-name . properties) (rest object)
    (loop for (nil prop-name prop-type) in properties
      for sharp-prop-name = (format nil "~:(~a~)" prop-name) ; C# property name
      for field-name = (concatenate 'string "_" prop-name) ; C# field name
      collect `(:arg ,prop-type ,prop-name) into init-args
      collect `(:setf (:id ,field-name) (:id ,prop-name)) into init
      nconc `((:field ,prop-type ,field-name)
          (:property ,prop-type ,sharp-prop-name
            (:get (:return (:id ,field-name)))
            (:set (:setf (:id ,field-name) (:id "value"))))) into props
      finally (return
            `(:class ,class-name
              (:constructor ,class-name ()
               (:comment "NOOP"))
              (:constructor ,class-name ,init-args
               ,@init)
              ,@props)))))

(defun generate-csharp-classes (data)
  "Generate C# classes from DSL spec"
  (destructuring-bind (ns-name . classes) (rest data)
    `(:namespace ,ns-name
      ,@(mapcar #'generate-csharp-class classes))))


generate-csharp-class генерит AST класса на основе
(object "Obj1" ...), а generate-csharp-classes генерит
всё вместе на основе (namespace "MyNS" ...):

;; глянем-ка на AST, интересно всё-таки

CL-USER> (generate-csharp-classes
      '(namespace "MyNS"
        (object "Obj1"
         (property "prop1" "int")
         (property "prop2" "string")
         (property "dblprop" "double"))
        (object "Obj2"
         (property "someprop" "object[]"))))
(:NAMESPACE "MyNS"
 (:CLASS "Obj1" (:CONSTRUCTOR "Obj1" NIL (:COMMENT "NOOP"))
  (:CONSTRUCTOR "Obj1"
   ((:ARG "int" "prop1") (:ARG "string" "prop2") (:ARG "double" "dblprop"))
   (:SETF (:ID "_prop1") (:ID "prop1")) (:SETF (:ID "_prop2") (:ID "prop2"))
   (:SETF (:ID "_dblprop") (:ID "dblprop")))
  (:FIELD "int" "_prop1")
  (:PROPERTY "int" "Prop1" (:GET (:RETURN (:ID "_prop1")))
   (:SET (:SETF (:ID "_prop1") (:ID "value"))))
  (:FIELD "string" "_prop2")
  (:PROPERTY "string" "Prop2" (:GET (:RETURN (:ID "_prop2")))
   (:SET (:SETF (:ID "_prop2") (:ID "value"))))
  (:FIELD "double" "_dblprop")
  (:PROPERTY "double" "Dblprop" (:GET (:RETURN (:ID "_dblprop")))
   (:SET (:SETF (:ID "_dblprop") (:ID "value")))))
 (:CLASS "Obj2" (:CONSTRUCTOR "Obj2" NIL (:COMMENT "NOOP"))
  (:CONSTRUCTOR "Obj2" ((:ARG "object[]" "someprop"))
   (:SETF (:ID "_someprop") (:ID "someprop")))
  (:FIELD "object[]" "_someprop")
  (:PROPERTY "object[]" "Someprop" (:GET (:RETURN (:ID "_someprop")))
   (:SET (:SETF (:ID "_someprop") (:ID "value"))))))

;; ну а теперь на шарп (* - последнее вёрнутое значение)

CL-USER> (p *)
namespace MyNS
{
    public class Obj1
    {
        Obj1()
        {
            // NOOP
        }

        Obj1(int prop1, string prop2, double dblprop)
        {
            _prop1 = prop1;
            _prop2 = prop2;
            _dblprop = dblprop;
        }

        private int _prop1;

        public int Prop1
        {
            get
            {
                return _prop1;
            }
            set
            {
                _prop1 = value;
            }
        }

        private string _prop2;

        public string Prop2
        {
            get
            {
                return _prop2;
            }
            set
            {
                _prop2 = value;
            }
        }

        private double _dblprop;

        public double Dblprop
        {
            get
            {
                return _dblprop;
            }
            set
            {
                _dblprop = value;
            }
        }
    }

    public class Obj2
    {
        Obj2()
        {
            // NOOP
        }

        Obj2(object[] someprop)
        {
            _someprop = someprop;
        }

        private object[] _someprop;

        public object[] Someprop
        {
            get
            {
                return _someprop;
            }
            set
            {
                _someprop = value;
            }
        }
    }
}
NIL

;; вуаля, блин!


У этого, понятное дело, может быть ещё куча применений —
можно генерить разные прокси классы, врапперы к базе/хранимкам и пр., в качестве источника
можно использовать .NET reflection напрямую через RDNZL, DBMS, можно парсить сырцы
через cl-yacc (в отличие от Yacc, препроцессинга он не требует, т.к. грамматика транслируется
макросами — отличный пример их применения, и отлаживается проще, т.к. есть TRACE) и т.д.
Короче говоря, R# на Лиспе написать куда как проще, чем на C#. Можно сделать мегакостыль для .NET.
Оговорка лишь одна — если можете писать на Лиспе, лучше пишите на Лиспе, это проще, чем
на C# с костылями, на чём бы эти костыли написаны не были. В общем и целом — ещё одно
замечательное применение Лиспа — dev tools.

Да, в исходнике я привёл ссылки на "вдохновителей", дублирую. Peter Norvig крут —
на таких, как он, Google держится PAIP читать ВСЕМ — хоть его в нашем краю
и достать не очень легко, оно того стоит — http://www.norvig.com/paip.html

;;;; Inspired by DPP (Dylan Pretty-Printer, a part of Peter Norvig's
;;;; Lisp to Dylan Converter — http://www.norvig.com/ltd/doc/ltd.html)
;;;; and by Dick Waters' paper "Some Useful Lisp Algorithms: Part 2"
;;;; (http://www.merl.com/publications/TR1993-017/)
;;;; See also: XP pretty-printer documentation in XP distribution
;;;; http://www-2.cs.cmu.edu/afs/cs.cmu.edu/project/ai-repository/ai/lang/lisp/code/io/xp/xp.tgz
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.