RubyKif Project (0.1 Beta)

The PowerLoom Knowledge Representation & Reasoning System is an intriguing tool which could conceivably be used by mechanized documentation systems such as Arti (in an analogous manner to the way MySQL works for Rails). That is, we could use PowerLoom as a server to store knowledge, retrieve it in "intelligent" ways, etc.

However, although there are some tools that allow Ruby to communicate with PowerLoom, the interface is not very natural, from Ruby's perspective. Specifically, the Ruby program is required to generate and interpret PowerLoom's variant of Knowledge Interchange Format (KIF) strings.

The RubyKif class currently handles low-level KIF serialization (eg, parsing, generation) issues in a reasonably robust (if not terribly sprightly) manner. It promises to handle still more (see below), but no guarantees (:-).

Overview

RubyKif is a Ruby class that assists in the use of KIF by Ruby programs. Specifically, it transforms KIF expressions from PowerLoom into Ruby data structures (and vice versa). I expect other methods to appear, however, over time...

Syntactically, KIF is simply a way of expressing "lists of lists", where the terminal tokens are numbers, operators, strings, and words. Happily, Ruby symbols handle operators and words gracefully; numbers and strings translate easily to their counterparts. So, using a text editor, it's pretty simple to transform KIF into Ruby:

( retrieve                [ :retrieve,
  10                        10,
  ( ?x    Person )          [ :'?x',  :Person ],
  ( happy ?x )              [ :happy, :'?x' ]
)                         ]

Using irb (interactive Ruby), we can manipulate the Ruby version:

>> k = [:retrieve, 10, [:'?x', :Person], [:happy, :'?x']]

>> puts k[2].inspect
[:'?x', :Person]

>> puts k[3][0].inspect
:happy

The Hard Part comes in when you want to do this transformation mechanically. Generation of KIF is trivial, but parsing is another story. KIF uses nested parentheses, quoted strings, two kinds of comments, etc. This makes it obnoxious to parse using regular expressions and Ruby code. So, we bring in a specialized tool.

The treetop parser generator, based on parsing expression grammars, is quite capable of handling the heavy lifting of KIF parsing. Basically, all we have to do is specify the grammar. Treetop will then generate a parser which we can use to parse any conformant KIF string. Because the strings we'll be parsing are generated by PowerLoom, we can expect both conformance and a high degree of regularity.

Translation Details

Tokens

Numbers and strings translate easily to their counterparts. Ruby symbols handle operators and words gracefully. If note (ie, comment) strings are retained, they are also saved as symbols (eg, :";...\n", :"#|...|#").

List levels

White space (eg, spaces, tabs, newlines) in KIF separates list items. RubyKif is fairly tolerant of missing or extra white space, where it doesn't cause ambiguity. When generating KIF, RubyKif separates each token from its neighbors by a single space.

Parentheses in KIF correspond to list levels in Ruby. Because the RubyKif load() method only returns a single list, incoming KIF is treated as if it had been wrapped in set parentheses. Thus, "foo bar" is parsed as "[ :foo, :bar ]".

KIF/Ruby Correspondence

Although the correspondence between KIF and Ruby is unambiguous and very easy to remember, reading the resulting symbols may cause some reader's eyes to cross. KIF symbols already approach line noise; adding a colon and a pair of quotes does nothing to improve things (eg, :':<<=>>').

KIF        Ruby             Notes
<=>        :'<=>'           sentop
:<=>       :':<=>'          defop

:exists    :exists          sentop
:axioms    :':axioms'       defop

; ...\n    :"; ...\n"       note (semi-colon comment)
#|...|#    :"#|...|#"       note (sharp-sign comment)

@abc       :'@abc'          surrogate symbol
?def       :'?def'          variable
ghi        :ghi             word

123        123              integer
1.2        1.2              float
1.2e1      12               float (exp. notation)

"Hi!"     "Hi!"             string

PowerLoom KIF

PowerLoom uses its own variant of KIF, as opposed to (say) vanilla ANSI KIF, KIF-3.0, or Ontolingua. I am aware of the following syntactic differences:

  • The "@" sigil may be used as the prefix to a word (eg, @foo), but it is used to identify "surrogate symbols", rather than "row variables".

  • Some words are case-sensitive (eg, Foo vs FOO).

  • Some words and symbols may be preceded by colons (eg, :foo, :=, :=>, :<=>).

  • Some words may be followed by question marks (eg, foo?, :bar?).

  • Qualified names may contain slashes (eg, PL-KERNEL-KB/RELATION).

Usage

Once all of the pieces are in place, using RubyKif should be pretty simple:

require 'ruby_kif'

@parser = RubyKif.new

kif     = '( happy ?x )'
ruby    = @parser.load(kif)

ruby = [ :happy, :'?x' ]
kif  = @parser.dump(ruby)

However, in order to get the pieces into place, I'm having to do quite a bit of development and testing. So, I've been using the RSpec tool to mechanize the process:

$ spec ruby_kif_spec.rb --format specdoc
RubyKif
- should parse '10'
...
- should load '10'
...
- should strip '10'
...
- should emit '10'
...

Finished in 0.936933 seconds

269 examples, 0 failures

Implementation

The core of the implementation seems to be basically complete. It passes 250+ tests (in ruby_kif_spec.rb) and (via kif_kb_parse) parses 4000+ lines of KIF knowledge base code:

Lines   Filename
 1731   kif_kb/aircraft.plm
  228   kif_kb/business.plm
  303   kif_kb/seismo/faults.plm
 1050   kif_kb/seismo/models.plm
   29   kif_kb/seismo/seismology-module-definition.plm
  924   kif_kb/seismo/seismology.plm
 4265   total

The implementation comprises about five hundred lines of code:

Lines   Filename           Description
   27   kif_kb_parse       Parse KIF KBs
  191   parse_kif.tt       KIF Grammar
   62   ruby_kif.rb        RubyKif class
  222   ruby_kif_spec.rb   RubyKif Spec
  502   total

RubyKif Class (ruby_kif.rb)

The ruby_kif.rb file defines the RubyKif class:

 

# ruby_kif.rb - RubyKif class
#
# Hacks:
#
#   Using globals is generally a Bad Idea (especially
#   for library code), but I don't know of a way to
#   pass an instance variable to Treetop rules, etc.

require 'rubygems'
require 'treetop'

Treetop.load('parse_kif.tt')

Trace    = false  # Trace     instance methods.
TraceAll = false  # Trace ALL instance methods.

class RubyKif

  def initialize
    @parser = KifGrammarParser.new
  end

  def dump(ruby) 
    case (cls = ruby.class.to_s)

    when 'Array'
      return ruby.inject('( ') \
        {|m, o| m + dump(o) + ' ' } + ')' 
    when 'Bignum';  return ruby.to_s 
    when 'Fixnum';  return ruby.to_s 
    when 'Float';   return ruby.to_s
    when 'String';  return '"' + ruby + '"' 
    when 'Symbol';  return ruby.to_s
    end

    puts "ERROR: ruby.class='#{  ruby.class}'"
    puts "ERROR: ruby.inspect='#{ruby.inspect}'"
  end

  def load(str) 
    @parser.parse(str).to_ruby
  end

  def parse(str) 
    $rk_strip = false
    out       = @parser.parse(str)

    if (Trace and !out)
      puts 'Instance methods:'
      tmp = Treetop::Runtime::CompiledParser.
        instance_methods(TraceAll)
      tmp.each {|line| puts '  ' + line }
    end
    return out 
  end

  def strip(str) 
    $rk_strip = true
    @parser.parse(str).to_ruby
  end

end

KIF Grammar (parse_kif.tt)

The parse_kif.tt file specifies the grammar that Treetop will use to construct the parser. This file is written in Treetop's Domain Specific Language (DSL), which mixes pattern-matching syntax with Ruby code:

 

# parse_kif.tt
#
#   http://treetop.rubyforge.org/
#   http://treetop.rubyforge.org/semantic_interpretation.html
#   http://treetop.rubyforge.org/syntactic_recognition.html
#   http://treetop.rubyforge.org/using_in_ruby.html
#   http://treetop.rubyforge.org/pitfalls_and_advanced_techniques.html
#
#   http://en.wikipedia.org/wiki/Parsing_expression_grammar
#
#   http://logic.stanford.edu/kif/dpans.html#4
#
# Many thanks to hagabaka and db-keen on #treetop !

grammar KifGrammar

  # The exp* methods and explanations are courtesy of db-keen on #treetop,
  # but any errors are almost undoubtedly my own.
  #
  # An exp_list is a space-separated list of exp_wraps.
  # An exp_wrap is either a parenthesized exp_list or a token.
  # The two rules are mutually recursive...
  #
  rule exp_list 
    space? exps:(exp_wrap ( space? exp_wrap )*)? space? { 

      # Let's walk through the expressions method.  The first thing it
      # does is call super(), which returns the syntax node given by the
      # 'exps' label above (because they have the same name).  
      #
      # Then, it fetches the elements of that syntax node, which is either
      # nil or an array of syntax nodes:  [ exp, ( space exp )* ]
      # (If elements returns a nil, we just hand back an empty array.)
      #
      # It's going to return an array, the first element of which is
      # simply that first exp:  els[0]
      #
      # Then it has to deal with els[1], which is:  ( space exp )*
      # els[1].elements returns an array:  [ space exp, space exp, ... ]
      #
      # So then we map that array to return just the second elements,
      # just the exps.  So we get:  [ exp, exp, exp, ....]
      #
      # Then we splat it into an array following the first exp.
      #
      def exps
        els = super.elements

        return [] unless (els)  # '()' -> []

        [ els[0], *els[1].elements.map{ |i| i.elements[1] } ]
      end

      def to_ruby
        out = []
        exps.each do |e|
          tmp = e.to_ruby
          out << tmp unless ($rk_strip and
                             tmp.class      == Symbol and
                             (tmp.to_s[0..1] == '#|' or  # note_sc
                              tmp.to_s[0..0] == ';'))    # note_sc
        end
        return out
      end
    }
  end

  rule exp_wrap 
    '(' exp_list ')' { 

      # The method_missing() and respond_to() defs just pass special
      # calls like to_ruby() on to exp_list().  This lets us ignore
      # the fact that exp_wrap() exists, from an API perspective.
      #
      def method_missing(m, *args); exp_list.send(m, *args);  end 

      def respond_to?(m);           exp_list.respond_to?(m);  end 
    } 
    / token
  end 
 
  # Notes (ie, comments)

  rule note_sb        # sharpsign-bar (ie, '#|...|#')
    '#|' n_b '|#'     { def to_ruby; text_value.to_sym;       end }
  end

  rule n_b
    (note_sb / !'#|' !'|#' .)*
  end

  rule note_sc        # semi_colon (ie, ";...\n")
    # !"\n" keeps the following '.' from matching a newline.
    # So, match a ';', followed by any number of things that
    # aren't newlines, followed by either a newline or EOF.
    #
    # The positive lookahead (&) keeps us from grabbing the
    # newline, so juxtaposed comment lines parse OK.

    ';' contents:(!"\n" .)* &("\n" / !.)  {
      def to_ruby
        (';' + contents.text_value + "\n").to_sym
      end
    }
  end

  rule note
    note_sb / note_sc
  end

  # Numbers (exponential, floating point, and integer numbers)

  rule b_f            # base - float
    b_i '.' b_i
  end

  rule b_i            # base - integer
    '-'? [0-9]+
  end 

  rule n_e
    b_f 'e' b_i !nd   { def to_ruby; text_value.to_f;        end }
    /
    b_i 'e' b_i !nd   { def to_ruby; text_value.to_f;        end }
  end

  rule n_f            # number - float
    b_f !nd           { def to_ruby; text_value.to_f;        end }
  end

  rule n_i            # number - integer
    b_i !nd           { def to_ruby; text_value.to_i;        end }
  end

  rule number
    n_e / n_f / n_i
  end 

  # Space (any of blank, formfeed, tab, newline, or return)

  rule space
    (' ' / "\f" / "\t" / "\n" / "\r")+
  end

  # Strings

  rule string
    # !'"' keeps the following '.' from matching a quote.

    '"' body:(!'"' . / '\"')* '"' {
      def to_ruby; body.text_value; end
    }
  end

  # Token (note, string, number, word)

  rule token
    note / string / number / word
  end

  # Word

  rule word
    wc (es / wc)*     { def to_ruby; text_value.to_sym;      end }
  end

  rule dgt
    [0-9]
  end

  rule es             # escape sequence
    '\\' .    { def to_ruby; '\\' + '%03o' % text_value[1];  end } 
  end

  rule etc
    [:!%&/<=>@_~] / '*' / '+' / '-' / '?' / '$' / '.'
  end

  rule ltr            # letter
    [a-zA-Z]
  end

  rule nd             # non-digit
    etc / ltr
  end

  rule wc             # word character
    dgt / nd          { def to_ruby; text_value;             end } 
  end

end

RubyKif Spec (ruby_kif_spec.rb)

The ruby_kif_spec.rb file defines the specifications which RubyKif is expected to meet. It tests both the parsing and emitting portions of the class:

 

# ruby_kif_spec.rb
#
# This is a set of specifications for the RubyKif class.
# To keep the file reasonably DRY, we first store the input
# and expected output strings in the KifTuples array, then
# walk through the array, trying to parse, load, etc.

require 'ruby_kif'

# We should be able to load and emit KIF, as:
#
#   ( retrieve                [ :retrieve,
#     10                        10,
#     ( ?x    Person )          [ :'?x',  :Person ],
#     ( happy ?x )              [ :happy, :'?x' ]
#   )                         ]

describe RubyKif do

  before(:each) do
    @parser = RubyKif.new
  end

  # KifTuples is a list of tuples.  Each tuple is a test
  # specification, containing three items:
  #
  #   test actions:  e(mit), l(oad), s(trip)
  #   KIF string
  #   Ruby value
  #
  # Every tuple in KifTuples may be tested in up to three
  # different ways, giving us an average of less than one
  # line of code (including comments!) per test.
  #
  # We emit the canonical form of KIF expressions, so the
  # e(mit) test action is restricted to these tuples.
  #
  KifTuples = [

    # Check out low-level functionality.

    # Terminals

    [ :els,  '10',                    [ 10                   ] ],
    [ :els,  '10.1',                  [ 10.1                 ] ],
    [ :_ls,  '10e1',                  [ 100.0                ] ],
    [ :_ls,  '10.1e1',                [ 101.0                ] ],
    [ :_ls,  '10.1e-1',               [ 1.01                 ] ],

    [ :els,  '-10',                   [ -10                  ] ],
    [ :els,  '-10.1',                 [ -10.1                ] ],
    [ :_ls,  '-10e1',                 [ -100.0               ] ],
    [ :_ls,  '-10.1e1',               [ -101.0               ] ],
    [ :_ls,  '-10.1e-1',              [ -1.01                ] ],

    [ :els,  '"foo"',                 [ 'foo'                ] ], 
    [ :els,  '@foo',                  [ :'@foo'              ] ],
    [ :els,  '?foo',                  [ :'?foo'              ] ],
    [ :els,  ':foo',                  [ :':foo'              ] ],
    [ :els,  'foo',                   [ :foo                 ] ], 

    [ :els,  '10foo',                 [ :'10foo'             ] ],
    [ :els,  'foo-2',                 [ :'foo-2'             ] ],
    [ :els,  'foo-2.5',               [ :'foo-2.5'           ] ],

    [ :_ls,  ' foo',                  [ :foo                 ] ],
    [ :_ls,  'foo ',                  [ :foo                 ] ],
    [ :_ls,  ' foo ',                 [ :foo                 ] ],
  
    [ :els,  '=',                     [ :'='                 ] ],
    [ :els,  '=>',                    [ :'=>'                ] ],
    [ :els,  '<=>',                   [ :'<=>'               ] ],
  
    [ :els,  ':=',                    [ :':='                ] ],
    [ :els,  ':=>',                   [ :':=>'               ] ],
    [ :els,  ':<=>',                  [ :':<=>'              ] ],
  
    # Zero- and One-element lists

    [ :_ls,  '()',                    [ [                  ] ] ],
    [ :els,  '( )',                   [ [                  ] ] ],

    [ :els,  '( 10 )',                [ [ 10               ] ] ],
    [ :_ls,  '( 10e1 )',              [ [ 100.0            ] ] ],
    [ :els,  '( 10.1 )',              [ [ 10.1             ] ] ],
    [ :_ls,  '( 10.1e1 )',            [ [ 101.0            ] ] ],
    [ :els,  '( "foo" )',             [ [ 'foo'            ] ] ], 
    [ :els,  '( @foo )',              [ [ :'@foo'          ] ] ],
    [ :els,  '( ?foo )',              [ [ :'?foo'          ] ] ],

    [ :_ls,  '(foo)',                 [ [ :foo             ] ] ],
    [ :_ls,  '( foo)',                [ [ :foo             ] ] ],
    [ :_ls,  '(foo )',                [ [ :foo             ] ] ],
    [ :els,  '( foo )',               [ [ :foo             ] ] ],
  
    [ :_ls,  ' (foo)',                [ [ :foo             ] ] ],
    [ :_ls,  ' (foo) ',               [ [ :foo             ] ] ],
    [ :_ls,  '(foo) ',                [ [ :foo             ] ] ],
  
    # Multiple-element lists

    [ :els,  '( 12 34 )',             [ [ 12, 34           ] ] ],
    [ :els,  '( 12 34 56 )',          [ [ 12,  34, 56      ] ] ], 
    [ :els,  '( foo bar )',           [ [ :foo,    :bar    ] ] ],
    [ :els,  '( @foo ?bar )',         [ [ :'@foo', :'?bar' ] ] ],

    [ :els,  ':foo "bar"',            [ :':foo',   'bar'   ] ],
    [ :_ls,  ':foo"bar"',             [ :':foo',   'bar'   ] ], 

    [ :_ls,  '(12 34)',               [ [ 12,      34      ] ] ],
    [ :_ls,  '(12 34 )',              [ [ 12,      34      ] ] ],
    [ :_ls,  '( 12 34)',              [ [ 12,      34      ] ] ], 


    # Nested lists

    [ :_ls,  '((12))',                [ [ [12]             ] ] ],
    [ :_ls,  '( (12) )',              [ [ [12]             ] ] ],
    [ :els,  '( ( 12 ) )',            [ [ [12]             ] ] ], 
    
    [ :_ls,  '(12 (34))',             [ [ 12,      [34]    ] ] ],
    [ :_ls,  '( 12 (34) )',           [ [ 12,      [34]    ] ] ],
    [ :els,  '( 12 ( 34 ) )',         [ [ 12,      [34]    ] ] ],
  
    # Add newlines

    [ :_ls,  "(@foo\n?bar)",          [ [ :'@foo', :'?bar' ] ] ],
    [ :_ls,  "\n(@foo\n?bar)",        [ [ :'@foo', :'?bar' ] ] ],
    [ :_ls,  "\n(@foo\n?bar)\n",      [ [ :'@foo', :'?bar' ] ] ],
    [ :_ls,  "(@foo\n?bar)\n",        [ [ :'@foo', :'?bar' ] ] ],

    [ :_ls,  "(@foo \n  ?bar)",       [ [ :'@foo', :'?bar' ] ] ],
    [ :_ls,  "\n(@foo \n  ?bar)",     [ [ :'@foo', :'?bar' ] ] ],
    [ :_ls,  "\n(@foo \n  ?bar)\n",   [ [ :'@foo', :'?bar' ] ] ],
    [ :_ls,  "(@foo \n  ?bar)\n",     [ [ :'@foo', :'?bar' ] ] ],

    # Add block comments

    [ :el_,  "( 1 #| X |# 2 )",    [ [ 1, :'#| X |#',    2 ] ] ],
    [ :el_,  "( 1 #| X\nX |# 2 )", [ [ 1, :"#| X\nX |#", 2 ] ] ], 

    [ :__s,  "( 1 #| X |# 2 )",    [ [ 1,                2 ] ] ],
    [ :__s,  "( 1 #| X\nX |# 2 )", [ [ 1,                2 ] ] ], 

    # Add notes (comments to EOL)

    [ :el_, ";notes...\n ( ?bar )",
      [ :";notes...\n", [ :'?bar' ] ] ], 

    [ :el_, "( @foo ;notes...\n ?bar )",
      [ [ :'@foo', :";notes...\n", :'?bar' ] ] ], 

    [ :el_, "( @foo ; notes... \n ?bar )",
      [ [ :'@foo', :"; notes... \n", :'?bar' ] ] ], 

    [ :el_, "( @foo ; more... \n ; notes... \n ?bar )",
      [ [ :'@foo', :"; more... \n", :"; notes... \n", :'?bar' ] ] ], 

    [ :_l_, "( @foo ; more... \n; notes... \n ?bar )",
      [ [ :'@foo', :"; more... \n", :"; notes... \n", :'?bar' ] ] ], 

    [ :el_, "( @foo ; more... \n ; notes... \n ( ?bar ) )",
      [ [ :'@foo', :"; more... \n", :"; notes... \n", [ :'?bar' ] ] ] ], 

    [ :__s, ";notes...\n (?bar)",
      [ [ :'?bar' ] ] ], 

    [ :__s, "(@foo ;notes...\n ?bar)",
      [ [ :'@foo', :'?bar' ] ] ], 

    [ :__s, "( @foo ; notes... \n ?bar)",
      [ [ :'@foo', :'?bar' ] ] ], 

    [ :__s, "( @foo ; more... \n ; notes... \n ?bar )",
      [ [ :'@foo', :'?bar' ] ] ], 

    # Try some real KIF.

    [ :els, '( retrieve 10 ( ?x Person ) ( happy ?x ) )',
      [ [:retrieve, 10, [ :'?x',  :Person ],
                        [ :happy, :'?x' ] ] ] ],
  ]

  # Can we parse the KIF strings?

  KifTuples.each do |i|
    it "should parse '#{ i[1].inspect[1..-2] }'" do
      result = @parser.parse(i[1])
      result.should_not be_nil
    end
  end

  # Are the loaded values correct?

  KifTuples.each do |i|
    next unless (i[0].to_s =~ /l/)
    it "should load '#{ i[1].inspect[1..-2] }'" do
      result = @parser.load(i[1])
      result.should == i[2]
    end
  end

  # Are the stripped values correct?

  KifTuples.each do |i|
    next unless (i[0].to_s =~ /s/)
    it "should strip '#{ i[1].inspect[1..-2] }'" do
      result = @parser.strip(i[1])
      result.should == i[2]
    end
  end

  # Can we emit the canonical forms?  

  KifTuples.each do |i|
    next unless (i[0].to_s =~ /e/)
    it "should emit '#{ i[2].inspect }'" do
      @parser.dump(i[2]).should == "( #{i[1]} )" # KLUDGE
    end
  end 

end

KIF Knowledge Bases (kif_kb_parse)

The PowerLoom project makes several KIF knowledge bases available on their web site. Parsing these has been an invaluable exercise for unearthing parser problems.

 

#!/usr/bin/env ruby
#
# kif_kb_parse - parse *.plm files below kif_kb

require 'ruby_kif'

@parser = RubyKif.new

files = Dir.glob('kif_kb/**/*.plm')

puts '*' * 60 #T

files.each do |file|
  puts file

  File.open(file) do |f|
    inp = f.read
    begin
      out = @parser.load(inp) 
      puts out.size
    rescue => e
      puts e
      next
    end
  end
  puts '*' * 60 #T
end

Status, Prospects, etc.

The core implementation seems to work pretty solidly, so I'm declaring it the 0.1 Beta release. Feel free to try it out; comments and suggestions are welcome...

Known Problems

  • The parser generates no diagnostic messages, by default. It is possible to turn on some tracing (by setting Trace and/or TraceAll - see ruby_kif.rb). However, the information returned is only useful to experts: it lists which internal parser routines got called.

  • The parser is no speed daemon. This isn't a problem for my current projects; if it becomes a problem for anyone, let me know. I'm sure we can come up with a faster parser.

Current Work

Using a BNF description of PowerLoom KIF, I hope to expand the current "list of lists" into an "annotated list" in which each node (whether list or token) is represented as a list whose first element is an attribute hash:
[ {...},
  [ {...}, :foo ],
  [ {...}, :bar ]

Among other things, the hash will contain bnf_type (eg, defop, listterm). More generally, this is a way to turn parsed KIF tokens into objects which can answer questions about themselves (eg, foo.termop?).

Note: If the bnf_type of a node cannot be characterized, a warning will be given about a mismatch between the grammar definition and the input KIF. A bit of iterative debugging should quickly expand the definition to handle most common input.

Using the annotated list as a basis, I hope to create a REXML "document", which I can then traverse using XPath, XQuery, etc. This gives us something akin to Ruby's Dir.glob() method or CSS's pattern matching syntax, only capable of handling assorted lists of lists (eg, KIF structures, S-expressions).

Some early (pre-alpha!) files in this effort include grammar_def.rb, grammar_use.rb, and try_grammar. My current thinking, however, is that I will end up moving to ANTLR (using Ruby as a target language) for these sorts of projects.

Further Reading

I have made a good start on a Resource List. It has sections for "AI in Ruby", ANTLR, "KIF, Lisp, Lists, etc.", PowerLoom, RSpec, and Treetop.


This wiki page is maintained by Rich Morin, an independent consultant specializing in software design, development, and documentation. Please feel free to email comments, inquiries, suggestions, etc!

Topic revision: r30 - 20 Jul 2008, RichMorin
This site is powered by FoswikiCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding CFCL Wiki? Send feedback