JavaでAST作るのは大変

コンパイラの実装言語にあると良い機能JavaでAST(抽象構文木)作るのが大変だという話に触れたのだけれども、実は自分の知らない良いやり方があるのではないかと思ってClojureなどJavaで実装された処理系のソースを覗いてみました。
結論:やっぱり大変。
Clojureの場合

% ls clojure-1.2.0-RC2/src/jvm/clojure/lang/
AFn.java                  ILookupSite.java                  Namespace.java
AFunction.java            ILookupThunk.java                 Numbers.java
AMapEntry.java            IMapEntry.java                    Obj.java
APersistentMap.java       IMeta.java                        PersistentArrayMap.java
APersistentSet.java       IObj.java                         PersistentHashMap.java
APersistentVector.java    IPersistentCollection.java        PersistentHashSet.java
ARef.java                 IPersistentList.java              PersistentList.java
AReference.java           IPersistentMap.java               PersistentQueue.java
ASeq.java                 IPersistentSet.java               PersistentStructMap.java
ATransientMap.java        IPersistentStack.java             PersistentTreeMap.java
ATransientSet.java        IPersistentVector.java            PersistentTreeSet.java
Agent.java                IProxy.java                       PersistentVector.java
ArrayChunk.java           IReduce.java                      ProxyHandler.java
ArraySeq.java             IRef.java                         RT.java
Associative.java          IReference.java                   Range.java
Atom.java                 ISeq.java                         Ratio.java
Binding.java              ITransientAssociative.java        Ref.java
Box.java                  ITransientCollection.java         Reflector.java
ChunkBuffer.java          ITransientMap.java                Repl.java
ChunkedCons.java          ITransientSet.java                RestFn.java
Compile.java              ITransientVector.java             Reversible.java
Compiler.java             Indexed.java                      Script.java
Cons.java                 IndexedSeq.java                   SeqEnumeration.java
Counted.java              IteratorSeq.java                  SeqIterator.java
Delay.java                Keyword.java                      Seqable.java
DynamicClassLoader.java   KeywordLookupSite.java            Sequential.java
EnumerationSeq.java       LazilyPersistentVector.java       Settable.java
Fn.java                   LazySeq.java                      Sorted.java
IChunk.java               LineNumberingPushbackReader.java  StringSeq.java
IChunkedSeq.java          LispReader.java                   Symbol.java
IDeref.java               LockingTransaction.java           TransactionalHashMap.java
IEditableCollection.java  MapEntry.java                     Util.java
IFn.java                  MapEquivalence.java               Var.java
IKeywordLookup.java       MethodImplCache.java              XMLHandler.java
ILookup.java              MultiFn.java
ILookupHost.java          Named.java

JRubyの場合

% ls jruby-1.5.1/src/org/jruby/ast/
AliasNode.java                    DStrNode.java                      NilNode.java
AndNode.java                      DSymbolNode.java                   Node.java
ArgAuxillaryNode.java             DVarNode.java                      NodeType.java
ArgsCatNode.java                  DXStrNode.java                     NonLocalControlFlowNode.java
ArgsNoArgNode.java                DefinedNode.java                   NotNode.java
ArgsNode.arities.erb              DefnNode.java                      NthRefNode.java
ArgsNode.erb                      DefsNode.java                      OpAsgnAndNode.java
ArgsNode.java                     DotNode.java                       OpAsgnNode.java
ArgsPreOneArgNode.java            EncodingNode.java                  OpAsgnOrNode.java
ArgsPreTwoArgNode.java            EnsureNode.java                    OpElementAsgnNode.java
ArgsPushNode.java                 EvStrNode.java                     OpElementOneArgAndAsgnNode.java
ArgumentNode.java                 FCallManyArgsBlockNode.java        OpElementOneArgAsgnNode.java
ArrayNode.java                    FCallManyArgsBlockPassNode.java    OpElementOneArgOrAsgnNode.java
AssignableNode.java               FCallManyArgsNode.java             OptArgNode.java
AttrAssignNode.java               FCallNoArgBlockNode.java           OrNode.java
AttrAssignOneArgNode.java         FCallNoArgBlockPassNode.java       PostExeNode.java
AttrAssignThreeArgNode.java       FCallNoArgNode.java                PreExeNode.java
AttrAssignTwoArgNode.java         FCallNode.java                     RedoNode.java
BackRefNode.java                  FCallOneArgBlockNode.java          RegexpNode.java
BeginNode.java                    FCallOneArgBlockPassNode.java      RescueBodyNode.java
BignumNode.java                   FCallOneArgNode.java               RescueNode.java
BinaryOperatorNode.java           FCallSpecialArgBlockNode.java      RestArgNode.java
BlockAcceptingNode.java           FCallSpecialArgBlockPassNode.java  RetryNode.java
BlockArg18Node.java               FCallSpecialArgNode.java           ReturnNode.java
BlockArgNode.java                 FCallThreeArgBlockNode.java        RootNode.java
BlockNode.java                    FCallThreeArgBlockPassNode.java    SClassNode.java
BlockPassNode.java                FCallThreeArgNode.java             SValue19Node.java
BreakNode.java                    FCallTwoArgBlockNode.java          SValueNode.java
CallManyArgsBlockNode.java        FCallTwoArgBlockPassNode.java      SelfNode.java
CallManyArgsBlockPassNode.java    FCallTwoArgNode.java               Splat19Node.java
CallManyArgsNode.java             FalseNode.java                     SplatNode.java
CallNoArgBlockNode.java           FileNode.java                      StarNode.java
CallNoArgBlockPassNode.java       FixnumNode.java                    StrNode.java
CallNoArgNode.java                FlipNode.java                      SuperNode.java
CallNode.java                     FloatNode.java                     SymbolNode.java
CallOneArgBlockNode.java          ForNode.java                       ToAryNode.java
CallOneArgBlockPassNode.java      GlobalAsgnNode.java                TrueNode.java
CallOneArgFixnumNode.java         GlobalVarNode.java                 TypedArgumentNode.java
CallOneArgNode.java               Hash19Node.java                    UndefNode.java
CallSpecialArgBlockNode.java      HashNode.java                      UnnamedRestArgNode.java
CallSpecialArgBlockPassNode.java  IArgumentNode.java                 UntilNode.java
CallSpecialArgNode.java           IScopingNode.java                  VAliasNode.java
CallThreeArgBlockNode.java        IfNode.java                        VCallNode.java
CallThreeArgBlockPassNode.java    InstAsgnNode.java                  WhenNode.java
CallThreeArgNode.java             InstVarNode.java                   WhenOneArgNode.java
CallTwoArgBlockNode.java          InvisibleNode.java                 WhileNode.java
CallTwoArgBlockPassNode.java      IterNode.java                      XStrNode.java
CallTwoArgNode.java               LambdaNode.java                    YieldNode.java
CaseNode.java                     ListNode.java                      YieldOneNode.java
ClassNode.java                    LiteralNode.java                   YieldThreeNode.java
ClassVarAsgnNode.java             LocalAsgnNode.java                 YieldTwoNode.java
ClassVarDeclNode.java             LocalVarNode.java                  ZArrayNode.java
ClassVarNode.java                 Match2Node.java                    ZSuperNode.java
Colon2ConstNode.java              Match3Node.java                    ZYieldNode.java
Colon2ImplicitNode.java           MatchNode.java                     ZeroArgNode.java
Colon2MethodNode.java             MethodDefNode.java                 executable
Colon2Node.java                   ModuleNode.java                    java_signature
Colon3Node.java                   MultipleAsgn19Node.java            types
ConstDeclNode.java                MultipleAsgnNode.java              util
ConstNode.java                    NewlineNode.java                   visitor
DAsgnNode.java                    NextNode.java
DRegexpNode.java                  NilImplicitNode.java

Jythonの場合

% ls src/org/python/core/
AbstractArray.java               PyClass.java                PyReflectedConstructor.java
AnnotationReader.java            PyClassMethod.java          PyReflectedField.java
ArgParser.java                   PyClassMethodDerived.java   PyReflectedFunction.java
AstList.java                     PyClassMethodDescr.java     PyReversedIterator.java
BaseSet.java                     PyCode.java                 PyRunnable.java
BuiltinDocs.java                 PyComplex.java              PyRunnableBootstrap.java
BytecodeLoader.java              PyComplexDerived.java       PySequence.java
ClassDictInit.java               PyCompoundCallable.java     PySequenceIter.java
ClasspathPyImporter.java         PyDataDescr.java            PySequenceList.java
ClasspathPyImporterDerived.java  PyDescriptor.java           PySet.java
CodeBootstrap.java               PyDictProxy.java            PySetDerived.java
CodeFlag.java                    PyDictionary.java           PySingleton.java
CodeLoader.java                  PyDictionaryDerived.java    PySlice.java
CompileMode.java                 PyEllipsis.java             PySlot.java
CompilerFacade.java              PyEnumerate.java            PyStaticMethod.java
CompilerFlags.java               PyEnumerateDerived.java     PyString.java
ContextGuard.java                PyException.java            PyStringDerived.java
ContextManager.java              PyFastSequenceIter.java     PyStringMap.java
Deriveds.java                    PyFile.java                 PySuper.java
FilelikeInputStream.java         PyFileDerived.java          PySuperDerived.java
FunctionThread.java              PyFileReader.java           PySyntaxError.java
FutureFeature.java               PyFileWriter.java           PySystemState.java
IdImpl.java                      PyFinalizableInstance.java  PyTableCode.java
InitModule.java                  PyFloat.java                PyTraceback.java
JavaImportHelper.java            PyFloatDerived.java         PyTuple.java
JavaImporter.java                PyFrame.java                PyTupleDerived.java
JythonInitializer.java           PyFrozenSet.java            PyType.java
MakeProxies.java                 PyFrozenSetDerived.java     PyTypeDerived.java
NewCompilerResources.java        PyFunction.java             PyUnicode.java
Opcode.java                      PyFunctionTable.java        PyUnicodeDerived.java
Options.java                     PyGenerator.java            PyXRange.java
ParserFacade.java                PyIgnoreMethodTag.java      PythonCodeBundle.java
Pragma.java                      PyIndentationError.java     PythonCompiler.java
PragmaReceiver.java              PyInstance.java             PythonTraceFunction.java
Py.java                          PyInteger.java              ReflectedArgs.java
PyArray.java                     PyIntegerDerived.java       ReflectedCallData.java
PyArrayDerived.java              PyIterator.java             SequenceIndexDelegate.java
PyBaseCode.java                  PyJavaPackage.java          Slotted.java
PyBaseException.java             PyJavaType.java             StderrWrapper.java
PyBaseExceptionDerived.java      PyList.java                 StdoutWrapper.java
PyBaseString.java                PyListDerived.java          SyspathArchive.java
PyBeanEvent.java                 PyLong.java                 SyspathJavaLoader.java
PyBeanEventProperty.java         PyLongDerived.java          ThreadState.java
PyBeanProperty.java              PyMethod.java               ThreadStateMapping.java
PyBoolean.java                   PyMethodDescr.java          TraceFunction.java
PyBuiltinCallable.java           PyModule.java               WrappedIterIterator.java
PyBuiltinClassMethodNarrow.java  PyModuleDerived.java        __builtin__.java
PyBuiltinFunction.java           PyNewWrapper.java           adapter
PyBuiltinFunctionNarrow.java     PyNone.java                 codecs.java
PyBuiltinFunctionSet.java        PyNotImplemented.java       exceptions.java
PyBuiltinMethod.java             PyObject.java               imp.java
PyBuiltinMethodNarrow.java       PyObjectDerived.java        io
PyBuiltinMethodSet.java          PyOverridableNew.java       packagecache
PyBytecode.java                  PyProperty.java             ucnhashAPI.java
PyCallIter.java                  PyPropertyDerived.java      util
PyCell.java                      PyProxy.java

ファイル名を見てもらえれば分かると思いますが、1ノードタイプ⇔1ファイルです。
何故こんなことになるかというと原因は二つあって

  1. Javaでは1クラスにつき1ファイル必要
  2. オブジェクト指向言語ではCompositeパターン木構造を実装する

です。Compositeパターンでは葉の種類毎に1クラスを作るので、抽象構文木のノードタイプ数だけファイルができる事になります。
ソースの自動生成とかの工夫はしているのかもしれないですが未確認です。

ちなみにVariantのある言語だと、例えばOCamlならファイルは一つで良くて(parsetree.mli)

% ls ocaml-3.11.2/parsing/
asttypes.mli  lexer.mll    linenum.mll  location.mli  longident.mli  parse.mli   parsetree.mli  printast.mli  syntaxerr.mli
lexer.mli     linenum.mli  location.ml  longident.ml  parse.ml       parser.mly  printast.ml    syntaxerr.ml

以下の様に書けます。

type expression =
  { pexp_desc: expression_desc;
    pexp_loc: Location.t }

and expression_desc =
    Pexp_ident of Longident.t
  | Pexp_constant of constant
  | Pexp_let of rec_flag * (pattern * expression) list * expression
  | Pexp_function of label * expression option * (pattern * expression) list
  | Pexp_apply of expression * (label * expression) list
  | Pexp_match of expression * (pattern * expression) list
  | Pexp_try of expression * (pattern * expression) list
  | Pexp_tuple of expression list
  | Pexp_construct of Longident.t * expression option * bool
  | Pexp_variant of label * expression option
  | Pexp_record of (Longident.t * expression) list * expression option
  | Pexp_field of expression * Longident.t
  | Pexp_setfield of expression * Longident.t * expression
  | Pexp_array of expression list
  | Pexp_ifthenelse of expression * expression * expression option
  | Pexp_sequence of expression * expression
  | Pexp_while of expression * expression
  | Pexp_for of string * expression * expression * direction_flag * expression
  | Pexp_constraint of expression * core_type option * core_type option
  | Pexp_when of expression * expression
  | Pexp_send of expression * string
  | Pexp_new of Longident.t
  | Pexp_setinstvar of string * expression
  | Pexp_override of (string * expression) list
  | Pexp_letmodule of string * module_expr * expression
  | Pexp_assert of expression
  | Pexp_assertfalse
  | Pexp_lazy of expression
  | Pexp_poly of expression * core_type option
  | Pexp_object of class_structure

brainfuckのコードを自動生成するコンパイラ

S式からbrainfuckのコードを自動生成するプログラムを書いてみました。
コード生成が目的ではなくて単にできるだろうかと気になったのでやってみました。gaucheを使いました。
brainfuck インタプリタをスタックマシンっぽく使って動作します。
変数が使えるところが個人的には面白いかもと思っています。
最適化とか全然考えていないので、大変非効率なコードが出ます。今後これ以上何か改良する予定もありません。

(define vtable `())
(define depth 0)
(define (incr) (set! depth (+ depth 1)))
(define (decr) (set! depth (- depth 1)))
(define (addvar name) (set! vtable (cons `(,name . ,depth) vtable)))
(define (delvar) (set! vtable (cdr vtable)))
(define (findvar name) (cdr (assoc name vtable)))

(define (putn c n)
  (let loop ((i 0))
    (if (< i n)
      (begin
        (display c)
        (loop (+ i 1))
        ))))

(define (set_int n)
  (if (> n 0)
    (if (<= n 20)
        (putn "+" n)
        (begin
          (display ">")
          (putn "+" (quotient n 10))
          (display "[<++++++++++>-]<")
          (putn "+" (remainder n 10))
          ))
    (if (<= -20 n)
      (putn "-" (- n))
      (begin
        (display ">")
        (putn "+" (quotient (- n) 10))
        (display "[<---------->-]<")
        (putn "-" (remainder (- n) 10))
        ))
    ))

(define (emit_int n)
  (begin
    (display ">")
    (set_int n)
    (incr)
    #t
    ))

(define (emit_puts str)
  (begin
    (display ">")
    (incr)
    (let ((n (string-length str)))
    (let loop ((i 0) (v 0))
      (if (< i n)
        (let ((c (string-byte-ref str i)))
          (begin
            (set_int (- c v))
            (display ".")
            (loop (+ i 1) c)
          ))
        )))
    (drop)
    #f
    ))

(define (emit_var name)
  (let ((n (- depth (findvar name))))
    (begin
      (putn "<" n)
      (display "[")
      (putn ">" (+ n 1))
      (display "+>+")
      (putn "<" (+ n 2))
      (display "-]")
      (putn ">" (+ n 2))
      (display "[")
      (putn "<" (+ n 2))
      (display "+")
      (putn ">" (+ n 2))
      (display "-]<")
      (incr)
      #t
      )))

(define (emit_assign name e)
  (let ((n (- depth (findvar name))))
    (begin
      (transl e)
      (decr)
      ; zero-clear
      (putn "<" (+ n 1))
      (display "[-]")
      (putn ">" (+ n 1))
      ; copy value
      (display "[")
      (putn "<" (+ n 1))
      (display "+")
      (putn ">" (+ n 1))
      (display "-]<")
      #f
      )))

(define (drop) (begin (display "[-]<") (decr)))

(define (transl expr)
  (cond
    ((integer? expr) (emit_int expr))
    ((char? expr)    (emit_int (char->integer expr)))
    ((eq? `get expr) (begin (display ">,") (incr) #t))
    ((symbol? expr)  (emit_var expr))
    ((eq? `put (car expr)) (begin (transl (cadr expr)) (display ".") (drop) #f))
    ((eq? `puts (car expr)) (emit_puts (cadr expr)))
    ((eq? `puti (car expr))
     (transl `(let v ,(cadr expr) (let x v
       (do
         (if (> x 100) (do (put (+ #\0 (/ v 100)) (set v (% v 100)))))
         (if (> x 10)  (do (put (+ #\0 (/ v 10)) (set v (% v 10)))))
         (put (+ #\0 v))
         )))))
    ((eq? `+ (car expr))
     (begin
       (transl (cadr expr))
       (transl (caddr expr))
       (display "[<+>-]<")
       (decr)
       #t
       ))
    ((eq? `- (car expr))
     (begin
       (transl (cadr expr))
       (transl (caddr expr))
       (display "[<->-]<")
       (decr)
       #t
       ))
    ((eq? `* (car expr))
     (transl `(let v 0 (let a ,(cadr expr) (let b ,(caddr expr)
       (do (while (!= b 0) (do (set v (+ v a)) (set b (- b 1)))) v))))))
    ((eq? `/ (car expr))
     (transl `(let v 0 (let a ,(cadr expr) (let b ,(caddr expr)
       (do (while (> a b) (do (set v (+ v 1)) (set a (- a b)))) v))))))
    ((eq? `% (car expr))
     (transl `(let a ,(cadr expr) (let b ,(caddr expr)
       (do (while (> a b) (set a (- a b))) a)))))
    ((eq? `sub (car expr))
     (begin
       (transl (cadr expr))
       (transl (caddr expr))
       (display "[<[>>+>+<<<-]>>>[<<<+>>>-]<[[-]<<->>]<-]<")
       (decr)
       #t
       ))
    ((eq? `!= (car expr)) (transl `(- ,(cadr expr) ,(caddr expr))))
    ((eq? `== (car expr))
     (begin
       (transl `(- ,(cadr expr) ,(caddr expr))))
       (emit_int 1)
       (display "<[>[-]<-]>[<+>-]<")
       (decr)
       )
    ((eq? `> (car expr)) (transl `(sub ,(cadr expr) ,(caddr expr))))
    ((eq? `< (car expr)) (transl `(sub ,(caddr expr) ,(cadr expr))))
    ((eq? `>= (car expr)) (transl `(sub (+ ,(cadr expr) 1) ,(caddr expr))))
    ((eq? `<= (car expr)) (transl `(sub (+ ,(caddr expr) 1) ,(cadr expr))))
    ((eq? `do (car expr))
     (let loop ((e (cdr expr)) (r #f))
       (if (pair? e)
         (begin
           (if r (drop))
           (loop (cdr e) (transl (car e)))
           )
         r)))
    ((eq? `while (car expr))
     (begin
       (transl (cadr expr))
       (display "[")
       (if (transl (caddr expr)) (drop))
       (drop)
       (transl (cadr expr))
       (display "]")
       (drop)
       #f
       ))
    ((eq? `if (car expr))
     (if (null? (cdddr expr))
       (transl `(if-nonzero ,(cadr expr) (caddr expr)))
       (transl `(let c ,(cadr expr) (do (if-nonzero c ,(caddr expr)) (if-zero c ,(cadddr expr)))))
       ))
    ((eq? `if-nonzero (car expr))
     (begin
       (transl (cadr expr))
       (display "[[-]")
       (if (transl (caddr expr)) (drop))
       (display "]")
       (drop)
       #f
       ))
    ((eq? `if-zero (car expr))
     (begin
       (transl (cadr expr))
       (emit_int 1)
       (display "<[>[-]<-]>[<+>-]<")
       (decr)
       (display "[[-]")
       (if (transl (caddr expr)) (drop))
       (display "]")
       (drop)
       #f
       ))
    ((eq? `let (car expr))
     (transl (caddr expr))
     (addvar (cadr expr))
     (if (transl (cadddr expr))
       (display "<[-]>[<+>-]<")
       )
     (delvar)
     #t
     )
    ((eq? `set (car expr)) (emit_assign (cadr expr) (caddr expr)))
    ))

(define (compile expr) (begin (transl expr) (display "\n")))

使い方

% gosh
gosh> (load "./bfgen.scm")
#t

表示

gosh> (compile `(put #\x))
>>++++++++++++[<++++++++++>-]<.[-]<
#<undef>
gosh> (compile `(puts "hello world"))
>>++++++++++[<++++++++++>-]<++++.---.+++++++..+++.>+++++++[<---------->-]<---------.>++++++++[<++++++++++>-]<+++++++.--------.+++.------.--------.[-]<
#<undef>

入力

gosh> (compile `(put get))
>,.[-]<
#<undef>

四則演算(+,-,*,/,%)。乗算は加算、除算は減算で実装しているのでめちゃくちゃ遅いです。

gosh> (compile `(put (+ #\0 3)))
>>++++[<++++++++++>-]<++++++++>+++[<+>-]<.[-]<
#<undef>

制御構造(do,if,while)。比較演算(==,!=,<,>,<=,>=)も用意しています。

gosh> (compile `(do (puts "hello") (puts " ") (puts "world")))
>>++++++++++[<++++++++++>-]<++++.---.+++++++..+++.[-]<>>+++[<++++++++++>-]<++.[-]<>>+++++++++++[<++++++++++>-]<+++++++++.--------.+++.------.--------.[-]<
#<undef>
gosh> (compile `(if (< 0 1) (puts "foo") (puts "bar")))
>+>[<[>>+>+<<<-]>>>[<<<+>>>-]<[[-]<<->>]<-]<[>+>+<<-]>>[<<+>>-]<[[-]>>++++++++++[<++++++++++>-]<++.+++++++++..[-]<][-]<[>+>+<<-]>>[<<+>>-]<>+<[>[-]<-]>[<+>-]<[[-]>>+++++++++[<++++++++++>-]<++++++++.-.+++++++++++++++++.[-]<][-]<
#<undef>
gosh> (compile `(while 1 (puts "foo")))
>+[>>++++++++++[<++++++++++>-]<++.+++++++++..[-]<[-]<>+][-]<
#<undef>

変数(定義はlet,破壊的代入はset)

gosh> (compile `(let x 1 (set x (+ x 1) (put #\0 x))))
>+[>+>+<<-]>>[<<+>>-]<>+[<+>-]<<[-]>[<+>-]<
#<undef>

以上を使うと例えばHello Worldを10行表示するコードとかをこんな感じで自動生成できます。

gosh> (compile `(let i 0 (while (< i 10) (do (puts "Hello World\n") (set i (+ i 1))))))
>>++++++++++<[>>+>+<<<-]>>>[<<<+>>>-]<[<[>>+>+<<<-]>>>[<<<+>>>-]<[[-]<<->>]<-]<[>>+++++++[<++++++++++>-]<++.>++[<++++++++++>-]<+++++++++.+++++++..+++.>+++++++[<---------->-]<---------.>+++++[<++++++++++>-]<+++++.>++[<++++++++++>-]<++++.+++.------.--------.>+++++++++[<---------->-]<.[-]<<[>>+>+<<<-]>>>[<<<+>>>-]<>+[<+>-]<<<[-]>>[<<+>>-]<[-]<>++++++++++<[>>+>+<<<-]>>>[<<<+>>>-]<[<[>>+>+<<<-]>>>[<<<+>>>-]<[[-]<<->>]<-]<][-]<
#<undef>

fizzbuzzとかも書けると思うけど数値の表示で100での除算とかが発生するから、遅すぎて使い物にならなそうです。

純粋関数型言語の処理系を作ってみることにした (その4 : Mark-2 G-machine)

純粋関数型言語の処理系を作ってみることにした (その3 : Mark-1 G-machine)の続き。

教科書P94-P97まで。
http://github.com/nineties/puref/commit/4cd5d92a5f02468929bc2dd2ddafbd363251c86c

Mark-2 G-machineでは遅延評価を行う為の準備として、Update/Popというインストラクションで前回のSlideインストラクションを置き換えます。
また、実行中のVMのインストラクション列・スタック・ヒープの状態を自動的に可視化する機能を実装しました。
ビルド

% git clone git://github.com/nineties/puref.git
% cd puref
% make

テストコードとしてExcercise 3.11を用意しています。

% cat test.pf
twice f x = f (f x);
id x = x;
main = twice twice id 3

-mark1,-mark2オプションで使用するG-machineを切り替えます。また-vis hogeVMの状態を可視化したhoge.pdfが生成されます。

% ./puref -mark1 -vis test_mark1 test.pf
output: 3
% ./puref -mark2 -vis test_mark2 test.pf
output: 3

また-dinsnでコンパイル結果を出力します。

% ./puref -dinsn test.pf
=== twice [2] ===
   1: Push 1
   2: Push 1
   3: Mkapp
   4: Push 1
   5: Mkapp
   6: Update 2
   7: Pop 2
   8: Unwind
=== id [1] ===
   1: Push 0
   2: Update 1
   3: Pop 1
   4: Unwind
=== main [0] ===
   1: Num 3
   2: SC id
   3: SC twice
   4: SC twice
   5: Mkapp
   6: Mkapp
   7: Mkapp
   8: Update 0
   9: Pop 0
  10: Unwind
output: 3

以下のpdfを読む際の参考になるかと思います。

生成されたpdfがこちら。全ステップを可視化しているので、枚数=ステップ数となります。
test_mark1.pdf
test_mark2.pdf

  • sc "hoge" はスーパーコンビネータ
  • appは関数適用ノード。赤いエッジが関数ノード。青いエッジが引数ノード。
  • *はただの間接ノード

Graphvizを使ったのですが、ノードの位置関係が入れ替わったりしてあまり見やすくないです。ごめんなさい。
で、mark1とmark2で何がどう変わったか説明しなければならないのですがそれはまた今度にします。

純粋関数型言語の処理系を作ってみることにした (その3 : Mark-1 G-machine)

純粋関数型言語の処理系を作ってみることにした (その2)の続き。
今日は教科書の第3章P75-P94(Mark1: A MINIMAL G-MACHINE)の実装をしました。
現状では関数適用しかできません。遅延評価はまだ扱っていなくて、次回Mark2で扱うことになると思います。
ただ、現時点で普通のスタックマシンと大分違っているので出来る限りの説明を試みます。

http://github.com/nineties/puref/
minimal G-machineの主要なソースコード

type instruction =
    | ScI of string
    | NumI of int
    | MkappI
    | PushI of int
    | SlideI of int
    | UnwindI

type node =
    | NumN of int
    | AppN of node * node
    | ScN of int * instruction list

let sctable = Hashtbl.create 0

let arg = function
    | AppN(f,arg) -> arg
    | _ -> failwith "arg: not reachable"

let rec drop ls n = if n == 0 then ls else drop (List.tl ls) (n - 1)

let rec interpret seq stack =
    match (seq, stack) with
    | [], NumN n::_ -> n
    | ScI name::is, _ -> begin
        try
            let m = Hashtbl.find sctable name in
            interpret is (m::stack)
        with Not_found -> failwith ("undefined super combinator: " ^ name)
    end
    | NumI n::is, _ -> interpret is (NumN n::stack)
    | MkappI::is, a0::a1::ss -> interpret is (AppN(a0, a1)::ss)
    | PushI n::is, _ ->
            let app = List.nth stack (n + 1) in
            interpret is (arg app::stack)
    | SlideI n::is, a0::ss  -> interpret is (a0::drop ss n)
    | UnwindI::_, NumN n::_ -> n
    | UnwindI::_, AppN(f,arg)::_ -> interpret seq (f::stack)
    | UnwindI::is, ScN(n,sc_seq)::s -> interpret sc_seq stack
    | _ -> failwith "interpret: not reachable"

let register_sc (name,narg,seq) = Hashtbl.add sctable name (ScN(narg,seq))
let register_scs scs = ignore( List.map register_sc scs )
let run_main () = interpret [ScI "main"; UnwindI] []

抽象構文木からインストラクション列へのコンパイラ

open Syntax
open Gmachine

let argpos name args =
    let rec f i ls = match ls with
        | [] -> failwith ("unknown variable: " ^ name)
        | v::_ when name = v -> i
        | _ -> f (i+1) (List.tl ls)
    in f 0 args

let rec compileC base args = function
    | VarE name when List.mem name args -> [PushI (base + argpos name args)]
    | VarE name -> [ScI name]
    | NumE num -> [NumI num]
    | AppE(e1,e2) -> compileC base args e2 @ compileC (base + 1) args e1 @ [MkappI]
    | _ -> failwith "not implemented"

let compileR args body =
    compileC 0 args body @ [SlideI (List.length args + 1); UnwindI]

let compileSC (name,args,body) =
    (name, List.length args, compileR args body)

let f prog = List.map compileSC prog

まず具体例。

nineties% cat test.pf
main = K 1 2;
K x y = x
nineties% ./puref < test.pf
main = K 1 2;
K x y = x
=== main [0] ===
   1: Num 2
   2: Num 1
   3: SC K
   4: Mkapp
   5: Mkapp
   6: Slide 1
   7: Unwind
=== K [2] ===
   1: Push 0
   2: Slide 3
   3: Unwind
result: 1

出力は抽象構文木・インストラクション・結果(整数値)です。普通の仮想マシンでは見慣れない命令が並んでいますが、その挙動を図解で説明します。図の左側が命令列(左端が先頭)、右側がスタック(右端がスタックトップ)です。
G-machineとは項書き換えシステムの実現方法の一つで、Unwindという命令が書き換えを実行する命令です。
まず、一番最初にスタックに"main"という項を積みます(SC main)。次のUnwindで、命令列がmainに束縛された物へと置き換えられます。

次にmainを右辺であるK 1 2に書き換えます。
まず引数を右から順番に積んで、Mkappによって関数適用ノードを作っていきます(1〜3行目)。
カリー化されているので、K 1 2は((K 1) 2)と2回の関数適用になります。
次にSlide命令でmainを取り除きます(4行目)。
最後に、関数適用ノードを展開して書き換え完了です(5〜7行目)。

最後の時点でのスタックの状態は下図のようになっています。従って、スタックをi+1個戻ったとこのAppノードの二番目の子供がi番目の引数になります。

K 1 2はまだ書き換えられるので続きます。Push nはn番目の引数をスタックに積む命令です。
最終的にスタックトップは1となり、これ以上書き換えられないのでこれで実行が完了となります。

以上のように、関数適用ノードを組み立てた後にUnwindで書き換えができなくなるまで書き換えて行くという事を繰り返します。
Unwindの規則を簡単にまとめると、

  • スタックトップが定数 : 終了
  • スタックトップがスーパーコンビネータ : 命令列を置き換える
  • スタックトップが関数適用(f aの形) : スタックトップにfを積む

となります。

後の章でもっと効率の良い実装が出てくると思いますが、基本的な考え方は同じになると思います。

brainfuckOSを作ろうとしてみた

こんなのを見つけたのだけど
http://code.google.com/p/brainfuckos/
中が空っぽでがっかりしたので、自分で何か作ってみようかと思った。
はっきり言ってめんどくさすぎるので続きはやりませんが、こんなの作ってみました。

.code16
.equ SCREEN, 0xb800
.equ WIDTH, 80
.equ HEIGHT, 25

    jmp entry

entry:
    movw    %cs, %ax
    movw    %ax, %ds
    movw    $SCREEN, %ax
    movw    %ax, %es
    movl    $0, %edi

    movl    $WIDTH*HEIGHT*2, %ecx
1:
    movb    $0, %es:(%edi)
    incl    %edi
    decl    %ecx
    jnz     1b

    movl    $0, %edi
    movl    $main, %edx

eval_loop:
    movb    (%edx), %al
    cmpb    $0, %al
    je      fin
    cmpb    $'>, %al
    je      goforward
    cmpb    $'<, %al
    je      gobackward
    cmpb    $'+, %al
    je      increment
    cmpb    $'-, %al
    je      decrement
    cmpb    $'[, %al
    je      beginloop
    jmp     endloop
    cmpb    $'], %al
    je      endloop
    jmp     error

goforward:
    incl    %edi
    incl    %edx
    jmp     eval_loop

gobackward:
    decl    %edi
    incl    %edx
    jmp     eval_loop

increment:
    incb    %es:(%edi)
    incl    %edx
    jmp     eval_loop

decrement:
    decb    %es:(%edi)
    incl    %edx
    jmp     eval_loop

beginloop:
    movl    $0, %ecx
    movb    %es:(%edi), %al
    cmpb    $0, %al
    je      2f
1:
    incl    %edx
    jmp     eval_loop
2:
    incl    %edx
    movb    (%edx), %al
    cmpb    $'], %al
    je      3f
    cmpb    $'[, %al
    je      4f
    jmp     2b
3:
    cmpl    $0, %ecx
    je      1b
    decl    %ecx
    jmp     2b
4:
    incl    %ecx
    jmp     2b

endloop:
    movl    $0, %ecx
    movb    %es:(%edi), %al
    cmpb    $0, %al
    jne     2f
1:
    incl    %edx
    jmp     eval_loop
2:
    decl    %edx
    movb    (%edx), %al
    cmpb    $'[, %al
    je      3f
    cmpb    $'], %al
    je      4f
    jmp     2b
3:
    cmpl    $0, %ecx
    je      1b
    decl    %ecx
    jmp     2b
4:
    incl    %ecx
    jmp     2b

error:
    movl    $0, %edi
1:
    movb    errmsg(%edi), %al
    cmpb    $0, %al
    je      fin
    movb    $0x06, %ah
    movw    %ax, %es:(,%edi,2)
    incl    %edi
    jmp     1b

fin:
    hlt
    jmp     fin

errmsg:
    .string  "unknown opcode"

main:
    .ascii ">++++++++[<+++++++++>-]+"
    .ascii ">+>++++++++++[<++++++++++>-]++"
    .ascii ">-->++++++++++[<+++++++++++>-]+++"
    .ascii ">-->++++++++++[<+++++++++++>-]++++"
    .ascii ">+>++++++++++[<+++++++++++>-]+++++"
    .ascii ">+++>+++++++"
    .byte  0

    .org    0x1fe
    .byte   0x55,0xaa

リンカスクリプト

OUTPUT_FORMAT(binary)
OUTPUT_ARCH(i386)

SECTIONS {
    . = 0x7c00;
    .text : { *(.text) }
}

ビルドしてQEMUとかで実行すると

となります。
brainfuckのメモリ空間として物理メモリを使えば画面に何か表示させたり出来る訳なので、0xb800(スクリーン)にマッピングして見たのが上のコード。512バイト制限を超えるのがめんどくさかったのでブートセクタのみでやってます。
brainfuckチューリング完全なので、特定のアドレスにシステムコールとか画面リフレッシュとかの機能を持たせる最小限のコードを書けば、原理的にはbrainfuckでOSが書けるはずです。この場合入出力オペレータ".と,"はいらないですが、適当なIOポートとかにつないで遊んでみても面白いかも。

オブジェクトファイルの設計

ファイル入出力とか可変長配列とかrowlVMのプリミティブが充実してきたので、現在はrowl-core言語で次期rowl1コンパイラの実装をしています。
ところが、メモリマネージャのないスクリプト言語であるrowl-coreは大量にメモリを消費する為ついに限界がきました。
そこで、この段階で分割コンパイルが出来るようにオブジェクトファイルの設計をしてリンカを作ります。ELFがどうなっているかとか全然調べてなくて適当です。
まず、rowlVMは基本的に相対アドレッシングなので全てのアドレスを書き換える必要はありません。以下をリンク時に修正する必要があります。

  1. ファイルをまたぐ関数呼び出し
  2. グローバルboxed変数のインデックス
  3. グローバルunboxed変数のアドレス

というわけでプログラムの先頭にシンボルテーブルを作成します。

シンボルテーブルの1エントリ

exportされるシンボルのテーブル(exportテーブル)

| シンボル長(2バイト) | シンボル | アドレス(4バイト) |

importされるテーブル(import テーブル)

| シンボル長(2バイト) | シンボル | 書き換え場所の数=N(4バイト) | 書き換え場所のアドレス(4バイト) x N |

書き換え場所のアドレスはバイトコードの先頭からの該当オペランドのオフセットです。

シンボルテーブル
| 1のエントリ数(4バイト) | 2のエントリ数(4バイト) | 3のエントリ数(4バイト) | エントリ0 | エントリ1 | ...
オブジェクトファイル

先頭から

  • ファイルの総バイト数(4バイト)
  • exportテーブル
  • importテーブル
  • グローバルboxedオブジェクトの数(4バイト)
  • グローバルunboxedオブジェクトの総バイト数(4バイト)
  • グローバルunboxedオブジェクト領域
  • バイトコード本体

純粋関数型言語の処理系を作ってみることにした (その2 : Pretty Printer)

純粋関数型言語の処理系を作ってみることにしたの続き。
先週末はちょっと忙しかったので毎週日曜日の予定がずれてしまいました。
今日はPretty Printerを作りました。G-machineは次回に回します。
http://github.com/nineties/puref/commit/9295d96e1e0adb73dafea06dd716c1aada6c3304
下がPretty Printerのコード。OCamlのFormatモジュールは書式指定文字列の中でボックスを作る事ができるのが特徴で、ちょっとおもしろいです。(@[ ... @])の部分。このボックスを使ってインデントとかの調整をします。
詳しくはhttp://caml.inria.fr/resources/doc/guides/format.html

open Format
open Syntax

let binop_string = function
     | Add -> "+"  | Sub -> "-"  | Mul -> "*"  | Div -> "/"  | Lt  -> "<"
     | Le  -> "<=" | Eq  -> "==" | Ne  -> "~=" | Ge  -> ">=" | Gt  -> ">"
     | And -> "&"  | Or  -> "    | "

let rec pp_vars ppf vars
    = List.iter (fun v -> fprintf ppf "%a@;" pp_print_string v) vars

let pp_break_list f ppf elems =
    pp_open_hvbox ppf 0;
    f ppf (List.hd elems);
    List.iter (fun def -> pp_print_break ppf 1 0; f ppf def) (List.tl elems);
    pp_close_box ppf ()

let rec pp_expr ppf = function
    | VarE id             -> pp_print_string ppf id
    | NumE num            -> pp_print_int ppf num
    | PackE (id,arity)    -> fprintf ppf "<%d,%d>" id arity
    | AppE (f,arg)        -> fprintf ppf "%a %a" pp_expr f pp_aexpr arg
    | InfixE (op,lhs,rhs)
    -> fprintf ppf "@[%a %s %a@]" pp_expr lhs (binop_string op) pp_expr rhs
    | LetE (defs,cont)
    -> fprintf ppf "@[@[<hv 4>let %a@]@;in@;%a@]" (pp_break_list pp_def) defs pp_expr cont
    | LetrecE (defs,cont)
    -> fprintf ppf "@[@[<hv 4>letrec@;%a@]@;in@;%a@]" (pp_break_list pp_def) defs pp_expr cont
    | CaseE (expr,alts)
    -> fprintf ppf "@[@[<hv 4>case %a@]@;of@;%a@]" pp_expr expr (pp_break_list pp_alt) alts
    | LambdaE (vars,body)
    -> fprintf ppf "@[@[<hv>%a>. %a@]" pp_vars vars pp_expr body

and pp_aexpr ppf exp = match exp with
    | VarE _ -> pp_expr ppf exp
    | NumE _ -> pp_expr ppf exp
    | PackE _ -> pp_expr ppf exp
    | _ -> fprintf ppf "(%a)" pp_expr exp

and pp_def ppf (var,expr)
    = fprintf ppf "@[%a = %a@]" pp_print_string var pp_expr expr

and pp_alt ppf (id,elems,cont)
    = fprintf ppf "@[<%d> %a-> %a@]" id pp_vars elems pp_expr cont

let pp_sc ppf (vars,body)
    = fprintf ppf "@[%a= %a@]" pp_vars vars pp_expr body

let rec pp_program ppf = function
    | [sc] -> pp_sc ppf sc; pp_print_newline ppf ()
    | sc::scs -> fprintf ppf "@[<v>%a;@;%a@]" pp_sc sc pp_program scs
    | _ -> failwith "not reachable"

これで、パーサの結果を出力することが出来ます。簡単なテストコードとして教科書10ページのstandard preludeを追加しました。

I x = x;
K x y = x;
K1 x y = y;
S f g x = f x (g x);
compose f g x = f (g x);
twice f = compose f f

実行結果

% ./puref < prelude.pf
I x = x;
K x y = x;
K1 x y = y;
S f g x = f x (g x);
compose f g x = f (g x);
twice f = compose f f

次回こそはG-machineの実装に入ります。