Repository: brixen/poetics Branch: master Commit: b382a804e3d3 Files: 26 Total size: 34.8 KB Directory structure: gitextract_l4a1w08_/ ├── .gitignore ├── Gemfile ├── LICENSE ├── README ├── Rakefile ├── bin/ │ └── poetics ├── lib/ │ ├── poetics/ │ │ ├── library/ │ │ │ └── code_loader.rb │ │ ├── library.rb │ │ ├── parser/ │ │ │ ├── parser.rb │ │ │ └── poetics.kpeg │ │ ├── parser.rb │ │ ├── syntax/ │ │ │ ├── ast.rb │ │ │ ├── literal.rb │ │ │ └── node.rb │ │ ├── syntax.rb │ │ └── version.rb │ └── poetics.rb ├── poetics.gemspec └── spec/ ├── custom/ │ ├── matchers/ │ │ └── parse_as.rb │ ├── runner/ │ │ └── relates.rb │ └── utils/ │ ├── options.rb │ └── script.rb ├── custom.rb ├── default.mspec ├── spec_helper.rb └── syntax/ └── literal_spec.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.rbc *.gem .bundle Gemfile.lock pkg/* ================================================ FILE: Gemfile ================================================ source "http://rubygems.org" gemspec gem "rake", ">= 0.8.7" gem "kpeg", "~> 0.8.2" gem "mspec", "~> 1.5.17" ================================================ FILE: LICENSE ================================================ Copyright (c) 2011 Brian Ford. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README ================================================ 1. What is Poetics? A native implementation of CoffeeScript [1] that runs on the Rubinius VM [2]. Q. Whence comes the name Poetics? A. Poetics is a partial anagram of the word CoffeeScript. It is also a nod to Jeremy Ashkenas, the author of CoffeeScript, and his interest in code as literature. Q. Why a native implementation? A. CoffeeScript is an interesting language in its own right. Rather than merely being a syntax layer on top of Javascript, and bound to expressing its semantics in those of Javascript, it deserves its own implementation. Many of the reasons to use CoffeeScript in Node.js would also apply to using this native implementation. 2. License Poetics is MIT licensed. See the LICENSE file. 3. Installation First, install Rubinius: 1. Using RVM (http://rvm.beginrescueend.com). rvm install rbx 2. Or directly (http://rubini.us/doc/en/what-is-rubinius/). Second, install Poetics: rbx -S gem install poetics 4. Running Poetics Poetics provides a REPL for exploratory programming or runs CoffeeScript scripts. rbx -S poetics -h The REPL presently just parses code and returns an S-Expression representation: $ rbx -S poetics > 42 [:number, 42.0, 1, 1] > "hello, y'all" [:string, "hello, y'all", 1, 1] > To exit the REPL hit CTRL + d. 5. Getting Help Poetics is a work in progress. If you encounter trouble, please file an issue at https://github.com/brixen/poetics/issues 6. Contributing If you find Poetics interesting and would like to help out, fork the project on Github and submit a pull request. 7. People Poetics was created by Brian Ford (brixen) to force him to really learn Javascript and CoffeeScript. 8. Credits Jeremy has created an very interesting language in CoffeeScript. This implementation steals bits and pieces from other projects: * Rubinius (https://github.com/rubinius/rubinius/) * KPeg (https://github.com/evanphx/kpeg/) * Atomy (https://github.com/vito/atomy/) * Poison (https://github.com/brixen/poison/) * Talon (https://github.com/evanphx/talon/) [1] CoffeeScript (http://jashkenas.github.com/coffee-script/) [2] Rubinius (http://rubini.us) ================================================ FILE: Rakefile ================================================ require 'bundler' Bundler::GemHelper.install_tasks task :default => :spec base = File.expand_path '../lib/poetics/parser', __FILE__ grammar = "#{base}/poetics.kpeg" parser = "#{base}/parser.rb" file parser => grammar do sh "rbx -S kpeg -f -s #{base}/poetics.kpeg -o #{base}/parser.rb" end desc "Convert the grammar description to a parser" task :parser => parser desc "Run the specs (default)" task :spec => :parser do sh "mspec spec" end ================================================ FILE: bin/poetics ================================================ #!/usr/bin/env rbx # # vim: filetype=ruby $:.unshift File.expand_path('../../lib', __FILE__) require 'readline' require 'poetics' class Poetics::Script HISTORY = File.expand_path "~/.poetics_history" attr_accessor :prompt def initialize @evals = [] @script = nil @history = [] end def options(argv=ARGV) options = Rubinius::Options.new "Usage: poetics [options] [script]", 20 options.on "-", "Read and evaluate code from STDIN" do @evals << STDIN.read end options.on "-c", "FILE", "Check the syntax of FILE" do |f| begin begin Poetics::Parser.parse_file f rescue => e e.render exit 1 end puts "Syntax OK" exit 0 end end options.on "-e", "CODE", "Execute CODE" do |e| @evals << e end options.on "-v", "--version", "Display the version" do puts Poetics::COMMAND_VERSION end options.on "-h", "--help", "Display this help" do puts options exit 0 end options.doc "" rest = options.parse(argv) @script ||= rest.first end def evals return if @evals.empty? Poetics::CodeLoader.evaluate @evals.join("\n") end def script return unless @script if File.exists? @script Poetics::CodeLoader.execute_file @script else STDERR.puts "Unable to find '#{@script}' to run" exit 1 end end def load_history return unless File.exists? HISTORY File.open HISTORY, "r" do |f| f.each { |l| Readline::HISTORY << l } end end def save_history File.open HISTORY, "w" do |f| @history.last(100).map { |s| f.puts s } end end def record_history(str) @history << str end def start_prompt @prompt = "> " end def continue_prompt @prompt = ". " end def repl return if @script load_history start_prompt begin snippet = "" while str = Readline.readline(prompt) break if str.nil? and snippet.empty? next if str.empty? snippet << str begin value = Poetics::CodeLoader.evaluate snippet p value record_history snippet snippet = "" start_prompt rescue => e STDERR.puts e end end ensure save_history end end def main options evals script repl end end Poetics::Script.new.main ================================================ FILE: lib/poetics/library/code_loader.rb ================================================ module Poetics class CodeLoader def self.evaluate(string) # We're just parsing for now Poetics::Parser.parse_to_sexp string end def self.execute_file(name) value = Poetics::Parser.parse_to_sexp IO.read(name) p value end end end ================================================ FILE: lib/poetics/library.rb ================================================ require 'poetics/library/code_loader' ================================================ FILE: lib/poetics/parser/parser.rb ================================================ class Poetics::Parser # STANDALONE START def setup_parser(str, debug=false) @string = str @pos = 0 @memoizations = Hash.new { |h,k| h[k] = {} } @result = nil @failed_rule = nil @failing_rule_offset = -1 setup_foreign_grammar end # This is distinct from setup_parser so that a standalone parser # can redefine #initialize and still have access to the proper # parser setup code. # def initialize(str, debug=false) setup_parser(str, debug) end attr_reader :string attr_reader :failing_rule_offset attr_accessor :result, :pos # STANDALONE START def current_column(target=pos) if c = string.rindex("\n", target-1) return target - c - 1 end target + 1 end def current_line(target=pos) cur_offset = 0 cur_line = 0 string.each_line do |line| cur_line += 1 cur_offset += line.size return cur_line if cur_offset >= target end -1 end def lines lines = [] string.each_line { |l| lines << l } lines end # def get_text(start) @string[start..@pos-1] end def show_pos width = 10 if @pos < width "#{@pos} (\"#{@string[0,@pos]}\" @ \"#{@string[@pos,width]}\")" else "#{@pos} (\"... #{@string[@pos - width, width]}\" @ \"#{@string[@pos,width]}\")" end end def failure_info l = current_line @failing_rule_offset c = current_column @failing_rule_offset if @failed_rule.kind_of? Symbol info = self.class::Rules[@failed_rule] "line #{l}, column #{c}: failed rule '#{info.name}' = '#{info.rendered}'" else "line #{l}, column #{c}: failed rule '#{@failed_rule}'" end end def failure_caret l = current_line @failing_rule_offset c = current_column @failing_rule_offset line = lines[l-1] "#{line}\n#{' ' * (c - 1)}^" end def failure_character l = current_line @failing_rule_offset c = current_column @failing_rule_offset lines[l-1][c-1, 1] end def failure_oneline l = current_line @failing_rule_offset c = current_column @failing_rule_offset char = lines[l-1][c-1, 1] if @failed_rule.kind_of? Symbol info = self.class::Rules[@failed_rule] "@#{l}:#{c} failed rule '#{info.name}', got '#{char}'" else "@#{l}:#{c} failed rule '#{@failed_rule}', got '#{char}'" end end class ParseError < RuntimeError end def raise_error raise ParseError, failure_oneline end def show_error(io=STDOUT) error_pos = @failing_rule_offset line_no = current_line(error_pos) col_no = current_column(error_pos) io.puts "On line #{line_no}, column #{col_no}:" if @failed_rule.kind_of? Symbol info = self.class::Rules[@failed_rule] io.puts "Failed to match '#{info.rendered}' (rule '#{info.name}')" else io.puts "Failed to match rule '#{@failed_rule}'" end io.puts "Got: #{string[error_pos,1].inspect}" line = lines[line_no-1] io.puts "=> #{line}" io.print(" " * (col_no + 3)) io.puts "^" end def set_failed_rule(name) if @pos > @failing_rule_offset @failed_rule = name @failing_rule_offset = @pos end end attr_reader :failed_rule def match_string(str) len = str.size if @string[pos,len] == str @pos += len return str end return nil end def scan(reg) if m = reg.match(@string[@pos..-1]) width = m.end(0) @pos += width return true end return nil end if "".respond_to? :getbyte def get_byte if @pos >= @string.size return nil end s = @string.getbyte @pos @pos += 1 s end else def get_byte if @pos >= @string.size return nil end s = @string[@pos] @pos += 1 s end end def parse(rule=nil) if !rule _root ? true : false else # This is not shared with code_generator.rb so this can be standalone method = rule.gsub("-","_hyphen_") __send__("_#{method}") ? true : false end end class LeftRecursive def initialize(detected=false) @detected = detected end attr_accessor :detected end class MemoEntry def initialize(ans, pos) @ans = ans @pos = pos @uses = 1 @result = nil end attr_reader :ans, :pos, :uses, :result def inc! @uses += 1 end def move!(ans, pos, result) @ans = ans @pos = pos @result = result end end def external_invoke(other, rule, *args) old_pos = @pos old_string = @string @pos = other.pos @string = other.string begin if val = __send__(rule, *args) other.pos = @pos other.result = @result else other.set_failed_rule "#{self.class}##{rule}" end val ensure @pos = old_pos @string = old_string end end def apply_with_args(rule, *args) memo_key = [rule, args] if m = @memoizations[memo_key][@pos] m.inc! prev = @pos @pos = m.pos if m.ans.kind_of? LeftRecursive m.ans.detected = true return nil end @result = m.result return m.ans else lr = LeftRecursive.new(false) m = MemoEntry.new(lr, @pos) @memoizations[memo_key][@pos] = m start_pos = @pos ans = __send__ rule, *args m.move! ans, @pos, @result # Don't bother trying to grow the left recursion # if it's failing straight away (thus there is no seed) if ans and lr.detected return grow_lr(rule, args, start_pos, m) else return ans end return ans end end def apply(rule) if m = @memoizations[rule][@pos] m.inc! prev = @pos @pos = m.pos if m.ans.kind_of? LeftRecursive m.ans.detected = true return nil end @result = m.result return m.ans else lr = LeftRecursive.new(false) m = MemoEntry.new(lr, @pos) @memoizations[rule][@pos] = m start_pos = @pos ans = __send__ rule m.move! ans, @pos, @result # Don't bother trying to grow the left recursion # if it's failing straight away (thus there is no seed) if ans and lr.detected return grow_lr(rule, nil, start_pos, m) else return ans end return ans end end def grow_lr(rule, args, start_pos, m) while true @pos = start_pos @result = m.result if args ans = __send__ rule, *args else ans = __send__ rule end return nil unless ans break if @pos <= m.pos m.move! ans, @pos, @result end @result = m.result @pos = m.pos return m.ans end class RuleInfo def initialize(name, rendered) @name = name @rendered = rendered end attr_reader :name, :rendered end def self.rule_info(name, rendered) RuleInfo.new(name, rendered) end # def setup_foreign_grammar; end # root = - value? - end def _root _save = self.pos while true # sequence _tmp = apply(:__hyphen_) unless _tmp self.pos = _save break end _save1 = self.pos _tmp = apply(:_value) unless _tmp _tmp = true self.pos = _save1 end unless _tmp self.pos = _save break end _tmp = apply(:__hyphen_) unless _tmp self.pos = _save break end _tmp = apply(:_end) unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_root unless _tmp return _tmp end # end = !. def _end _save = self.pos _tmp = get_byte _tmp = _tmp ? nil : true self.pos = _save set_failed_rule :_end unless _tmp return _tmp end # - = (" " | "\t" | "\n")* def __hyphen_ while true _save1 = self.pos while true # choice _tmp = match_string(" ") break if _tmp self.pos = _save1 _tmp = match_string("\t") break if _tmp self.pos = _save1 _tmp = match_string("\n") break if _tmp self.pos = _save1 break end # end choice break unless _tmp end _tmp = true set_failed_rule :__hyphen_ unless _tmp return _tmp end # value = (string | number | boolean) def _value _save = self.pos while true # choice _tmp = apply(:_string) break if _tmp self.pos = _save _tmp = apply(:_number) break if _tmp self.pos = _save _tmp = apply(:_boolean) break if _tmp self.pos = _save break end # end choice set_failed_rule :_value unless _tmp return _tmp end # boolean = position (true | false | null | undefined) def _boolean _save = self.pos while true # sequence _tmp = apply(:_position) unless _tmp self.pos = _save break end _save1 = self.pos while true # choice _tmp = apply(:_true) break if _tmp self.pos = _save1 _tmp = apply(:_false) break if _tmp self.pos = _save1 _tmp = apply(:_null) break if _tmp self.pos = _save1 _tmp = apply(:_undefined) break if _tmp self.pos = _save1 break end # end choice unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_boolean unless _tmp return _tmp end # true = "true" {true_value} def _true _save = self.pos while true # sequence _tmp = match_string("true") unless _tmp self.pos = _save break end @result = begin; true_value; end _tmp = true unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_true unless _tmp return _tmp end # false = "false" {false_value} def _false _save = self.pos while true # sequence _tmp = match_string("false") unless _tmp self.pos = _save break end @result = begin; false_value; end _tmp = true unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_false unless _tmp return _tmp end # null = "null" {null_value} def _null _save = self.pos while true # sequence _tmp = match_string("null") unless _tmp self.pos = _save break end @result = begin; null_value; end _tmp = true unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_null unless _tmp return _tmp end # undefined = "undefined" {undefined_value} def _undefined _save = self.pos while true # sequence _tmp = match_string("undefined") unless _tmp self.pos = _save break end @result = begin; undefined_value; end _tmp = true unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_undefined unless _tmp return _tmp end # number = position (real | hex | int) def _number _save = self.pos while true # sequence _tmp = apply(:_position) unless _tmp self.pos = _save break end _save1 = self.pos while true # choice _tmp = apply(:_real) break if _tmp self.pos = _save1 _tmp = apply(:_hex) break if _tmp self.pos = _save1 _tmp = apply(:_int) break if _tmp self.pos = _save1 break end # end choice unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_number unless _tmp return _tmp end # hexdigits = /[0-9A-Fa-f]/ def _hexdigits _tmp = scan(/\A(?-mix:[0-9A-Fa-f])/) set_failed_rule :_hexdigits unless _tmp return _tmp end # hex = "0x" < hexdigits+ > {hexadecimal(text)} def _hex _save = self.pos while true # sequence _tmp = match_string("0x") unless _tmp self.pos = _save break end _text_start = self.pos _save1 = self.pos _tmp = apply(:_hexdigits) if _tmp while true _tmp = apply(:_hexdigits) break unless _tmp end _tmp = true else self.pos = _save1 end if _tmp text = get_text(_text_start) end unless _tmp self.pos = _save break end @result = begin; hexadecimal(text); end _tmp = true unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_hex unless _tmp return _tmp end # digits = ("0" | /[1-9]/ /[0-9]/*) def _digits _save = self.pos while true # choice _tmp = match_string("0") break if _tmp self.pos = _save _save1 = self.pos while true # sequence _tmp = scan(/\A(?-mix:[1-9])/) unless _tmp self.pos = _save1 break end while true _tmp = scan(/\A(?-mix:[0-9])/) break unless _tmp end _tmp = true unless _tmp self.pos = _save1 end break end # end sequence break if _tmp self.pos = _save break end # end choice set_failed_rule :_digits unless _tmp return _tmp end # int = < digits > {number(text)} def _int _save = self.pos while true # sequence _text_start = self.pos _tmp = apply(:_digits) if _tmp text = get_text(_text_start) end unless _tmp self.pos = _save break end @result = begin; number(text); end _tmp = true unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_int unless _tmp return _tmp end # real = < digits "." digits ("e" /[-+]/? /[0-9]/+)? > {number(text)} def _real _save = self.pos while true # sequence _text_start = self.pos _save1 = self.pos while true # sequence _tmp = apply(:_digits) unless _tmp self.pos = _save1 break end _tmp = match_string(".") unless _tmp self.pos = _save1 break end _tmp = apply(:_digits) unless _tmp self.pos = _save1 break end _save2 = self.pos _save3 = self.pos while true # sequence _tmp = match_string("e") unless _tmp self.pos = _save3 break end _save4 = self.pos _tmp = scan(/\A(?-mix:[-+])/) unless _tmp _tmp = true self.pos = _save4 end unless _tmp self.pos = _save3 break end _save5 = self.pos _tmp = scan(/\A(?-mix:[0-9])/) if _tmp while true _tmp = scan(/\A(?-mix:[0-9])/) break unless _tmp end _tmp = true else self.pos = _save5 end unless _tmp self.pos = _save3 end break end # end sequence unless _tmp _tmp = true self.pos = _save2 end unless _tmp self.pos = _save1 end break end # end sequence if _tmp text = get_text(_text_start) end unless _tmp self.pos = _save break end @result = begin; number(text); end _tmp = true unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_real unless _tmp return _tmp end # string = position "\"" < /[^\\"]*/ > "\"" {string_value(text)} def _string _save = self.pos while true # sequence _tmp = apply(:_position) unless _tmp self.pos = _save break end _tmp = match_string("\"") unless _tmp self.pos = _save break end _text_start = self.pos _tmp = scan(/\A(?-mix:[^\\"]*)/) if _tmp text = get_text(_text_start) end unless _tmp self.pos = _save break end _tmp = match_string("\"") unless _tmp self.pos = _save break end @result = begin; string_value(text); end _tmp = true unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_string unless _tmp return _tmp end # line = { current_line } def _line @result = begin; current_line ; end _tmp = true set_failed_rule :_line unless _tmp return _tmp end # column = { current_column } def _column @result = begin; current_column ; end _tmp = true set_failed_rule :_column unless _tmp return _tmp end # position = line:l column:c { position(l, c) } def _position _save = self.pos while true # sequence _tmp = apply(:_line) l = @result unless _tmp self.pos = _save break end _tmp = apply(:_column) c = @result unless _tmp self.pos = _save break end @result = begin; position(l, c) ; end _tmp = true unless _tmp self.pos = _save end break end # end sequence set_failed_rule :_position unless _tmp return _tmp end Rules = {} Rules[:_root] = rule_info("root", "- value? - end") Rules[:_end] = rule_info("end", "!.") Rules[:__hyphen_] = rule_info("-", "(\" \" | \"\\t\" | \"\\n\")*") Rules[:_value] = rule_info("value", "(string | number | boolean)") Rules[:_boolean] = rule_info("boolean", "position (true | false | null | undefined)") Rules[:_true] = rule_info("true", "\"true\" {true_value}") Rules[:_false] = rule_info("false", "\"false\" {false_value}") Rules[:_null] = rule_info("null", "\"null\" {null_value}") Rules[:_undefined] = rule_info("undefined", "\"undefined\" {undefined_value}") Rules[:_number] = rule_info("number", "position (real | hex | int)") Rules[:_hexdigits] = rule_info("hexdigits", "/[0-9A-Fa-f]/") Rules[:_hex] = rule_info("hex", "\"0x\" < hexdigits+ > {hexadecimal(text)}") Rules[:_digits] = rule_info("digits", "(\"0\" | /[1-9]/ /[0-9]/*)") Rules[:_int] = rule_info("int", "< digits > {number(text)}") Rules[:_real] = rule_info("real", "< digits \".\" digits (\"e\" /[-+]/? /[0-9]/+)? > {number(text)}") Rules[:_string] = rule_info("string", "position \"\\\"\" < /[^\\\\\"]*/ > \"\\\"\" {string_value(text)}") Rules[:_line] = rule_info("line", "{ current_line }") Rules[:_column] = rule_info("column", "{ current_column }") Rules[:_position] = rule_info("position", "line:l column:c { position(l, c) }") end ================================================ FILE: lib/poetics/parser/poetics.kpeg ================================================ %% name = Poetics::Parser root = - value? - end end = !. - = (" " | "\t" | "\n")* value = string | number | boolean boolean = position ( true | false | null | undefined) true = "true" ~true_value false = "false" ~false_value null = "null" ~null_value undefined = "undefined" ~undefined_value number = position ( real | hex | int ) hexdigits = /[0-9A-Fa-f]/ hex = '0x' < hexdigits+ > ~hexadecimal(text) digits = '0' | /[1-9]/ /[0-9]/* int = < digits > ~number(text) real = < digits '.' digits ('e' /[-+]/? /[0-9]/+)? > ~number(text) string = position '"' < /[^\\"]*/ > '"' ~string_value(text) # keep track of column and line line = { current_line } column = { current_column } position = line:l column:c { position(l, c) } ================================================ FILE: lib/poetics/parser.rb ================================================ require 'poetics/parser/parser' class Poetics::Parser include Poetics::Syntax def self.parse_to_sexp(string) parser = new string unless parser.parse parser.raise_error end parser.result.to_sexp end attr_reader :line, :column def position(line, column) @line = line @column = column end end ================================================ FILE: lib/poetics/syntax/ast.rb ================================================ module Poetics module Syntax def number(value) Number.new line, column, value end def hexadecimal(value) Number.new line, column, value.to_i(16) end def true_value True.new line, column end def false_value False.new line, column end def null_value Null.new line, column end def undefined_value Undefined.new line, column end def string_value(value) String.new line, column, value end end end ================================================ FILE: lib/poetics/syntax/literal.rb ================================================ module Poetics module Syntax class Value < Node def to_sexp [sexp_name, value, line, column] end end class Number < Value attr_accessor :value def initialize(line, column, value) super @value = value.to_f end def sexp_name :number end end class Boolean < Node def to_sexp [sexp_name, line, column] end end class True < Boolean def sexp_name :true end end class False < Boolean def sexp_name :false end end class Null < Boolean def sexp_name :null end end class Undefined < Boolean def sexp_name :undefined end end class String < Value attr_accessor :value def initialize(line, column, text) super @value = text end def sexp_name :string end end end end ================================================ FILE: lib/poetics/syntax/node.rb ================================================ module Poetics module Syntax class Node attr_accessor :line, :column def initialize(line, column, *) @line = line @column = column end def to_sexp end end end end ================================================ FILE: lib/poetics/syntax.rb ================================================ require 'poetics/syntax/node' require 'poetics/syntax/literal' require 'poetics/syntax/ast' ================================================ FILE: lib/poetics/version.rb ================================================ module Poetics VERSION = "0.0.1" RELEASE_DATE = "yyyy-mm-dd" COMMAND_VERSION = "poetics #{VERSION} (1.1.0 #{RELEASE_DATE})" end ================================================ FILE: lib/poetics.rb ================================================ require 'poetics/version' require 'poetics/syntax' require 'poetics/parser' require 'poetics/library' ================================================ FILE: poetics.gemspec ================================================ # -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "poetics/version" Gem::Specification.new do |s| s.name = "poetics" s.version = Poetics::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Brian Ford"] s.email = ["brixen@gmail.com"] s.homepage = "https://github.com/brixen/poetics" s.summary = %q{A native implementation of CoffeeScript on the Rubinius VM} s.description =<<-EOD Poetics implements CoffeeScript (http://jashkenas.github.com/coffee-script/) directly on the Rubinius VM (http://rubini.us). It includes a REPL for exploratory programming, as well as executing CoffeeScript scripts directly. EOD s.files = `git ls-files`.split("\n") s.test_files = Dir["spec/**/*.rb"] s.executables = ["poetics"] s.require_paths = ["lib"] end ================================================ FILE: spec/custom/matchers/parse_as.rb ================================================ class ParseAsMatcher def initialize(expected) @expected = expected end def matches?(actual) @actual = Poetics::Parser.parse_to_sexp actual @actual == @expected end def failure_message ["Expected:\n#{@actual.inspect}\n", "to equal:\n#{@expected.inspect}"] end end class Object def parse_as(sexp) ParseAsMatcher.new sexp end end ================================================ FILE: spec/custom/runner/relates.rb ================================================ # NOTE: Copied from Rubinius # # SpecDataRelation enables concise specs that involve several different forms # of the same data. This is specifically useful for the parser and compiler # specs where the output of each stage is essentially related to the input # Ruby source. Together with the #relates spec method, it enables specs like: # # describe "An If node" do # relates "a if b" do # parse do # # return the expected sexp # end # # compile do |g| # # return the expected bytecode # end # # jit do |as| # # return the expected asm/machine code # end # end # # relates "if a; b; end" do # # ... # end # end class SpecDataRelation # Provides a simple configurability so that any one or more of the possible # processes can be run. See the custom options in custom/utils/options.rb. def self.enable(process) @processors ||= [] @processors << process end # Returns true if no process is specifically set or if +process+ is in the # list of enabled processes. In other words, all processes are enabled by # default, or any combination of them may be enabled. def self.enabled?(process) @processors.nil? or @processors.include?(process) end def initialize(ruby) @ruby = ruby end # Formats the Ruby source code for reabable output in the -fs formatter # option. If the source contains no newline characters, wraps the source in # single quotes to set if off from the rest of the description string. If # the source does contain newline characters, sets the indent level to four # characters. def format(ruby) if /\n/ =~ ruby lines = ruby.rstrip.to_a if /( *)/ =~ lines.first if $1.size > 4 dedent = $1.size - 4 ruby = lines.map { |l| l[dedent..-1] }.join else indent = " " * (4 - $1.size) ruby = lines.map { |l| "#{indent}#{l}" }.join end end "\n#{ruby}" else "'#{ruby}'" end end # Creates spec example blocks if the compile process is enabled. def compile(*plugins, &block) return unless self.class.enabled? :compiler ruby = @ruby it "is compiled from #{format ruby}" do generator = Rubinius::TestGenerator.new generator.instance_eval(&block) ruby.should compile_as(generator, *plugins) end end def parse(&block) return unless self.class.enabled? :parser ruby = @ruby it "is parsed from #{format ruby}" do ruby.should parse_as(block.call) end end end class Object def relates(str, &block) SpecDataRelation.new(str).instance_eval(&block) end end ================================================ FILE: spec/custom/utils/options.rb ================================================ # Custom MSpec options # class MSpecOptions def compiler # The require is inside the method because this file has to be able to be # loaded in MRI and there are parts of the custom ensemble that are # Rubinius specific (primarily iseq, which could potentially be fixed by # better structuring the compiler). require 'spec/custom/runner/relates' on("--compiler", "Run only the compile part of the compiler specs") do SpecDataRelation.enable :compiler end end def parser on("--parser", "Run only the parse part of the compiler specs") do SpecDataRelation.enable :parser end end end ================================================ FILE: spec/custom/utils/script.rb ================================================ # Custom options for mspec-run # class MSpecRun def custom_options(options) options.compiler options.parser end end # Custom options for mspec-ci # class MSpecCI def custom_options(options) options.compiler options.parser end end # Custom options for mspec-tag # class MSpecTag def custom_options(options) options.compiler options.parser end end ================================================ FILE: spec/custom.rb ================================================ require 'spec/custom/runner/relates' require 'spec/custom/matchers/parse_as' require 'spec/custom/utils/options' require 'spec/custom/utils/script' ================================================ FILE: spec/default.mspec ================================================ # vim: filetype=ruby require 'spec/custom' class MSpecScript set :target, 'rbx' end ================================================ FILE: spec/spec_helper.rb ================================================ $: << File.expand_path('../../lib', __FILE__) require 'poetics' ================================================ FILE: spec/syntax/literal_spec.rb ================================================ require 'spec/spec_helper' describe "The Number node" do relates "42" do parse { [:number, 42.0, 1, 1] } end relates " 42" do parse { [:number, 42.0, 1, 2] } end relates "42 " do parse { [:number, 42.0, 1, 1] } end relates "1.23" do parse { [:number, 1.23, 1, 1] } end relates "0x2a" do parse { [:number, 42.0, 1, 1] } end end describe "The True node" do relates "true" do parse { [:true, 1, 1] } end end describe "The False node" do relates "false" do parse { [:false, 1, 1] } end end describe "The Null node" do relates "null" do parse { [:null, 1, 1] } end end describe "The Undefined node" do relates "undefined" do parse { [:undefined, 1, 1] } end end describe "The String node" do relates '"hello, world"' do parse { [:string, "hello, world", 1, 1] } end relates <<-ruby do "hello" ruby parse { [:string, "hello", 1, 7] } end end