The Great Purge. Removing the Ruby compiler, and all of its accoutrements. bin/coffee is now CoffeeScript-in-CoffeeScript
This commit is contained in:
parent
edf5f4947e
commit
6446e0004c
2
Cakefile
2
Cakefile
|
@ -3,7 +3,7 @@ coffee: require 'coffee-script'
|
|||
|
||||
# Run a CoffeeScript through our node/coffee interpreter.
|
||||
run: (args) ->
|
||||
proc: process.createChildProcess 'bin/node_coffee', args
|
||||
proc: process.createChildProcess 'bin/coffee', args
|
||||
proc.addListener 'error', (err) -> if err then puts err
|
||||
|
||||
|
||||
|
|
14
README
14
README
|
@ -23,7 +23,7 @@
|
|||
CoffeeScript is a little language that compiles into JavaScript.
|
||||
|
||||
Install the compiler:
|
||||
gem install coffee-script
|
||||
... to be determined ...
|
||||
|
||||
Compile a script:
|
||||
coffee /path/to/script.coffee
|
||||
|
@ -38,14 +38,4 @@
|
|||
|
||||
The source repository:
|
||||
git://github.com/jashkenas/coffee-script.git
|
||||
|
||||
Warning: A new version of the compiler, written entirely in CoffeeScript,
|
||||
is in the works. Check out /src for the details. To try it out, use
|
||||
bin/node_coffee. To have CoffeeScript recompile itself, run:
|
||||
|
||||
build compiler
|
||||
|
||||
To rebuild the Jison parser (takes about 30 seconds), run:
|
||||
|
||||
build grammar
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#!/usr/bin/env ruby
|
||||
#!/usr/bin/env node --
|
||||
|
||||
require "#{File.dirname(__FILE__)}/../lib/coffee_script/command_line.rb"
|
||||
process.mixin(require('sys'));
|
||||
|
||||
CoffeeScript::CommandLine.new
|
||||
require.paths.unshift('./lib/coffee_script');
|
||||
|
||||
require('command_line').run();
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/env node --
|
||||
|
||||
process.mixin(require('sys'));
|
||||
|
||||
require.paths.unshift('./lib/coffee_script');
|
||||
|
||||
require('command_line').run();
|
|
@ -1,27 +0,0 @@
|
|||
Gem::Specification.new do |s|
|
||||
s.name = 'coffee-script'
|
||||
s.version = '0.3.2' # Keep version in sync with coffee-script.rb
|
||||
s.date = '2010-2-8'
|
||||
|
||||
s.homepage = "http://jashkenas.github.com/coffee-script/"
|
||||
s.summary = "The CoffeeScript Compiler"
|
||||
s.description = <<-EOS
|
||||
CoffeeScript is a little language that compiles into JavaScript. Think
|
||||
of it as JavaScript's less ostentatious kid brother -- the same genes,
|
||||
roughly the same height, but a different sense of style. Apart from a
|
||||
handful of bonus goodies, statements in CoffeeScript correspond
|
||||
one-to-one with their equivalent in JavaScript, it's just another
|
||||
way of saying it.
|
||||
EOS
|
||||
|
||||
s.authors = ['Jeremy Ashkenas']
|
||||
s.email = 'jashkenas@gmail.com'
|
||||
s.rubyforge_project = 'coffee-script'
|
||||
s.has_rdoc = false
|
||||
|
||||
s.require_paths = ['lib']
|
||||
s.executables = ['coffee']
|
||||
|
||||
s.files = Dir['bin/*', 'examples/*', 'extras/**/*', 'lib/**/*',
|
||||
'coffee-script.gemspec', 'LICENSE', 'README', 'package.json']
|
||||
end
|
|
@ -1,8 +1,8 @@
|
|||
This folder includes rough cuts of CoffeeScript syntax highlighters for
|
||||
TextMate and Vim. Improvements to their lexing ability are always welcome.
|
||||
|
||||
To install the TextMate bundle, run `bin/coffee --install-bundle`, or drop it
|
||||
into "~/Library/Application Support/TextMate/Bundles".
|
||||
To install the TextMate bundle, drop it into:
|
||||
~/Library/Application Support/TextMate/Bundles
|
||||
|
||||
To install the Vim highlighter, copy "coffee.vim" into the "syntax" directory of
|
||||
your vim72, and enable it in either of the following two ways:
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
||||
require "coffee_script/lexer"
|
||||
require "coffee_script/parser"
|
||||
require "coffee_script/nodes"
|
||||
require "coffee_script/value"
|
||||
require "coffee_script/scope"
|
||||
require "coffee_script/rewriter"
|
||||
require "coffee_script/parse_error"
|
||||
|
||||
# Namespace for all CoffeeScript internal classes.
|
||||
module CoffeeScript
|
||||
|
||||
VERSION = '0.3.2' # Keep in sync with the gemspec.
|
||||
|
||||
# Compile a script (String or IO) to JavaScript.
|
||||
def self.compile(script, options={})
|
||||
script = script.read if script.respond_to?(:read)
|
||||
Parser.new.parse(script).compile(options)
|
||||
end
|
||||
|
||||
end
|
|
@ -1,235 +0,0 @@
|
|||
require 'optparse'
|
||||
require 'fileutils'
|
||||
require 'open3'
|
||||
begin
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../coffee-script')
|
||||
rescue LoadError => e
|
||||
puts(e.message)
|
||||
puts("use \"rake build:parser\" to regenerate parser.rb")
|
||||
exit(1)
|
||||
end
|
||||
|
||||
module CoffeeScript
|
||||
|
||||
# The CommandLine handles all of the functionality of the `coffee`
|
||||
# utility.
|
||||
class CommandLine
|
||||
|
||||
BANNER = <<-EOS
|
||||
coffee compiles CoffeeScript source files into JavaScript.
|
||||
|
||||
Usage:
|
||||
coffee path/to/script.coffee
|
||||
EOS
|
||||
|
||||
# Seconds to pause between checks for changed source files.
|
||||
WATCH_INTERVAL = 0.5
|
||||
|
||||
# Path to the root of the CoffeeScript install.
|
||||
ROOT = File.expand_path(File.dirname(__FILE__) + '/../..')
|
||||
|
||||
# Commands to execute CoffeeScripts.
|
||||
RUNNERS = {
|
||||
:node => "node #{ROOT}/lib/coffee_script/runner.js",
|
||||
:narwhal => "narwhal -p #{ROOT} -e 'require(\"coffee-script\").run(system.args);'"
|
||||
}
|
||||
|
||||
# Run the CommandLine off the contents of ARGV.
|
||||
def initialize
|
||||
@mtimes = {}
|
||||
parse_options
|
||||
return launch_repl if @options[:interactive]
|
||||
return eval_scriptlet if @options[:eval]
|
||||
check_sources
|
||||
return run_scripts if @options[:run]
|
||||
@sources.each {|source| compile_javascript(source) }
|
||||
watch_coffee_scripts if @options[:watch]
|
||||
end
|
||||
|
||||
# The "--help" usage message.
|
||||
def usage
|
||||
puts "\n#{@option_parser}\n"
|
||||
exit
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
# Compiles (or partially compiles) the source CoffeeScript file, returning
|
||||
# the desired JS, tokens, or lint results.
|
||||
def compile_javascript(source)
|
||||
script = File.read(source)
|
||||
return tokens(script) if @options[:tokens]
|
||||
js = compile(script, source)
|
||||
return unless js
|
||||
return puts(js) if @options[:print]
|
||||
return lint(js) if @options[:lint]
|
||||
File.open(path_for(source), 'w+') {|f| f.write(js) }
|
||||
end
|
||||
|
||||
# Spins up a watcher thread to keep track of the modification times of the
|
||||
# source files, recompiling them whenever they're saved.
|
||||
def watch_coffee_scripts
|
||||
watch_thread = Thread.start do
|
||||
loop do
|
||||
@sources.each do |source|
|
||||
mtime = File.stat(source).mtime
|
||||
@mtimes[source] ||= mtime
|
||||
if mtime > @mtimes[source]
|
||||
@mtimes[source] = mtime
|
||||
compile_javascript(source)
|
||||
end
|
||||
end
|
||||
sleep WATCH_INTERVAL
|
||||
end
|
||||
end
|
||||
Signal.trap("INT") { watch_thread.kill }
|
||||
watch_thread.join
|
||||
end
|
||||
|
||||
# Ensure that all of the source files exist.
|
||||
def check_sources
|
||||
usage if @sources.empty?
|
||||
missing = @sources.detect {|s| !File.exists?(s) }
|
||||
if missing
|
||||
STDERR.puts("File not found: '#{missing}'")
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
# Pipe compiled JS through JSLint (requires a working 'jsl' command).
|
||||
def lint(js)
|
||||
stdin, stdout, stderr = Open3.popen3('jsl -nologo -stdin')
|
||||
stdin.write(js)
|
||||
stdin.close
|
||||
puts stdout.read.tr("\n", '')
|
||||
errs = stderr.read.chomp
|
||||
puts errs unless errs.empty?
|
||||
stdout.close and stderr.close
|
||||
end
|
||||
|
||||
# Eval a little piece of CoffeeScript directly from the command line.
|
||||
def eval_scriptlet
|
||||
script = STDIN.tty? ? @sources.join(' ') : STDIN.read
|
||||
return tokens(script) if @options[:tokens]
|
||||
js = compile(script)
|
||||
return lint(js) if @options[:lint]
|
||||
puts js
|
||||
end
|
||||
|
||||
# Use Node.js or Narwhal to run an interactive CoffeeScript session.
|
||||
def launch_repl
|
||||
exec "#{RUNNERS[@options[:runner]]}"
|
||||
rescue Errno::ENOENT
|
||||
puts "Error: #{@options[:runner]} must be installed to use the interactive REPL."
|
||||
exit(1)
|
||||
end
|
||||
|
||||
# Use Node.js or Narwhal to compile and execute CoffeeScripts.
|
||||
def run_scripts
|
||||
sources = @sources.join(' ')
|
||||
exec "#{RUNNERS[@options[:runner]]} #{sources}"
|
||||
rescue Errno::ENOENT
|
||||
puts "Error: #{@options[:runner]} must be installed in order to execute scripts."
|
||||
exit(1)
|
||||
end
|
||||
|
||||
# Print the tokens that the lexer generates from a source script.
|
||||
def tokens(script)
|
||||
puts Lexer.new.tokenize(script).inspect
|
||||
end
|
||||
|
||||
# Compile a single source file to JavaScript.
|
||||
def compile(script, source='error')
|
||||
begin
|
||||
options = {}
|
||||
options[:no_wrap] = true if @options[:no_wrap]
|
||||
options[:globals] = true if @options[:globals]
|
||||
CoffeeScript.compile(script, options)
|
||||
rescue CoffeeScript::ParseError => e
|
||||
STDERR.puts "#{source}: #{e.message}"
|
||||
exit(1) unless @options[:watch]
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Write out JavaScript alongside CoffeeScript unless an output directory
|
||||
# is specified.
|
||||
def path_for(source)
|
||||
filename = File.basename(source, File.extname(source)) + '.js'
|
||||
dir = @options[:output] || File.dirname(source)
|
||||
File.join(dir, filename)
|
||||
end
|
||||
|
||||
# Install the CoffeeScript TextMate bundle to ~/Library.
|
||||
def install_bundle
|
||||
bundle_dir = File.expand_path('~/Library/Application Support/TextMate/Bundles/')
|
||||
FileUtils.cp_r("#{ROOT}/extras/CoffeeScript.tmbundle", bundle_dir)
|
||||
end
|
||||
|
||||
# Use OptionParser for all the options.
|
||||
def parse_options
|
||||
@options = {:runner => :node}
|
||||
@option_parser = OptionParser.new do |opts|
|
||||
opts.on('-i', '--interactive', 'run an interactive CoffeeScript REPL') do |i|
|
||||
@options[:interactive] = true
|
||||
end
|
||||
opts.on('-r', '--run', 'compile and run a CoffeeScript') do |r|
|
||||
@options[:run] = true
|
||||
end
|
||||
opts.on('-o', '--output [DIR]', 'set the directory for compiled JavaScript') do |d|
|
||||
@options[:output] = d
|
||||
FileUtils.mkdir_p(d) unless File.exists?(d)
|
||||
end
|
||||
opts.on('-w', '--watch', 'watch scripts for changes, and recompile') do |w|
|
||||
@options[:watch] = true
|
||||
end
|
||||
opts.on('-p', '--print', 'print the compiled JavaScript to stdout') do |d|
|
||||
@options[:print] = true
|
||||
end
|
||||
opts.on('-l', '--lint', 'pipe the compiled JavaScript through JSLint') do |l|
|
||||
@options[:lint] = true
|
||||
end
|
||||
opts.on('-e', '--eval', 'compile a cli scriptlet or read from stdin') do |e|
|
||||
@options[:eval] = true
|
||||
end
|
||||
opts.on('-t', '--tokens', 'print the tokens that the lexer produces') do |t|
|
||||
@options[:tokens] = true
|
||||
end
|
||||
opts.on('-v', '--verbose', 'print at every step of code generation') do |v|
|
||||
ENV['VERBOSE'] = 'true'
|
||||
end
|
||||
opts.on('-n', '--no-wrap', 'raw output, no function safety wrapper') do |n|
|
||||
@options[:no_wrap] = true
|
||||
end
|
||||
opts.on('-g', '--globals', 'attach all top-level variables as globals') do |n|
|
||||
@options[:globals] = true
|
||||
end
|
||||
opts.on_tail('--narwhal', 'use Narwhal instead of Node.js') do |n|
|
||||
@options[:runner] = :narwhal
|
||||
end
|
||||
opts.on_tail('--install-bundle', 'install the CoffeeScript TextMate bundle') do |i|
|
||||
install_bundle
|
||||
exit
|
||||
end
|
||||
opts.on_tail('--version', 'display CoffeeScript version') do
|
||||
puts "CoffeeScript version #{CoffeeScript::VERSION}"
|
||||
exit
|
||||
end
|
||||
opts.on_tail('-h', '--help', 'display this help message') do
|
||||
usage
|
||||
end
|
||||
end
|
||||
@option_parser.banner = BANNER
|
||||
begin
|
||||
@option_parser.parse!(ARGV)
|
||||
rescue OptionParser::InvalidOption => e
|
||||
puts e.message
|
||||
exit(1)
|
||||
end
|
||||
@sources = ARGV
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -1,483 +0,0 @@
|
|||
class Parser
|
||||
|
||||
# Declare terminal tokens produced by the lexer.
|
||||
token IF ELSE UNLESS
|
||||
token NUMBER STRING REGEX
|
||||
token TRUE FALSE YES NO ON OFF
|
||||
token IDENTIFIER PROPERTY_ACCESS PROTOTYPE_ACCESS SOAK_ACCESS
|
||||
token CODE PARAM_START PARAM PARAM_END NEW RETURN
|
||||
token CALL_START CALL_END INDEX_START INDEX_END
|
||||
token TRY CATCH FINALLY THROW
|
||||
token BREAK CONTINUE
|
||||
token FOR IN OF BY WHEN WHILE
|
||||
token SWITCH LEADING_WHEN
|
||||
token DELETE INSTANCEOF TYPEOF
|
||||
token SUPER EXTENDS
|
||||
token ASSIGN RETURN
|
||||
token NEWLINE
|
||||
token COMMENT
|
||||
token JS
|
||||
token INDENT OUTDENT
|
||||
|
||||
# Declare order of operations.
|
||||
prechigh
|
||||
left '?'
|
||||
nonassoc UMINUS UPLUS NOT '!' '!!' '~' '++' '--'
|
||||
left '*' '/' '%' '.'
|
||||
left '+' '-'
|
||||
left '<<' '>>' '>>>' '&' '|' '^'
|
||||
left '<=' '<' '>' '>='
|
||||
right DELETE INSTANCEOF TYPEOF
|
||||
right '==' '!=' IS ISNT
|
||||
left '&&' '||' AND OR
|
||||
right '-=' '+=' '/=' '*=' '%=' '||=' '&&=' '?='
|
||||
right INDENT
|
||||
left OUTDENT
|
||||
right WHEN LEADING_WHEN IN OF BY
|
||||
right THROW FOR NEW SUPER
|
||||
left EXTENDS
|
||||
right ASSIGN RETURN
|
||||
right '->' '=>' UNLESS IF ELSE WHILE
|
||||
preclow
|
||||
|
||||
rule
|
||||
|
||||
# All parsing will end in this rule, being the trunk of the AST.
|
||||
Root:
|
||||
/* nothing */ { result = Expressions.new }
|
||||
| Terminator { result = Expressions.new }
|
||||
| Expressions { result = val[0] }
|
||||
| Block Terminator { result = val[0] }
|
||||
;
|
||||
|
||||
# Any list of expressions or method body, seperated by line breaks or semis.
|
||||
Expressions:
|
||||
Expression { result = Expressions.wrap(val) }
|
||||
| Expressions Terminator Expression { result = val[0] << val[2] }
|
||||
| Expressions Terminator { result = val[0] }
|
||||
;
|
||||
|
||||
# All types of expressions in our language. The basic unit of CoffeeScript
|
||||
# is the expression.
|
||||
Expression:
|
||||
Value
|
||||
| Call
|
||||
| Code
|
||||
| Operation
|
||||
| Assign
|
||||
| If
|
||||
| Try
|
||||
| Throw
|
||||
| Return
|
||||
| While
|
||||
| For
|
||||
| Switch
|
||||
| Extends
|
||||
| Splat
|
||||
| Existence
|
||||
| Comment
|
||||
;
|
||||
|
||||
# A block of expressions. Note that the Rewriter will convert some postfix
|
||||
# forms into blocks for us, by altering the token stream.
|
||||
Block:
|
||||
INDENT Expressions OUTDENT { result = val[1] }
|
||||
| INDENT OUTDENT { result = Expressions.new }
|
||||
;
|
||||
|
||||
# Tokens that can terminate an expression.
|
||||
Terminator:
|
||||
"\n"
|
||||
| ";"
|
||||
;
|
||||
|
||||
# All hard-coded values. These can be printed straight to JavaScript.
|
||||
Literal:
|
||||
NUMBER { result = LiteralNode.new(val[0]) }
|
||||
| STRING { result = LiteralNode.new(val[0]) }
|
||||
| JS { result = LiteralNode.new(val[0]) }
|
||||
| REGEX { result = LiteralNode.new(val[0]) }
|
||||
| BREAK { result = LiteralNode.new(val[0]) }
|
||||
| CONTINUE { result = LiteralNode.new(val[0]) }
|
||||
| TRUE { result = LiteralNode.new(Value.new(true)) }
|
||||
| FALSE { result = LiteralNode.new(Value.new(false)) }
|
||||
| YES { result = LiteralNode.new(Value.new(true)) }
|
||||
| NO { result = LiteralNode.new(Value.new(false)) }
|
||||
| ON { result = LiteralNode.new(Value.new(true)) }
|
||||
| OFF { result = LiteralNode.new(Value.new(false)) }
|
||||
;
|
||||
|
||||
# Assignment to a variable (or index).
|
||||
Assign:
|
||||
Value ASSIGN Expression { result = AssignNode.new(val[0], val[2]) }
|
||||
;
|
||||
|
||||
# Assignment within an object literal (can be quoted).
|
||||
AssignObj:
|
||||
IDENTIFIER ASSIGN Expression { result = AssignNode.new(ValueNode.new(val[0]), val[2], :object) }
|
||||
| STRING ASSIGN Expression { result = AssignNode.new(ValueNode.new(LiteralNode.new(val[0])), val[2], :object) }
|
||||
| NUMBER ASSIGN Expression { result = AssignNode.new(ValueNode.new(LiteralNode.new(val[0])), val[2], :object) }
|
||||
| Comment { result = val[0] }
|
||||
;
|
||||
|
||||
# A return statement.
|
||||
Return:
|
||||
RETURN Expression { result = ReturnNode.new(val[1]) }
|
||||
| RETURN { result = ReturnNode.new(ValueNode.new(Value.new('null'))) }
|
||||
;
|
||||
|
||||
# A comment.
|
||||
Comment:
|
||||
COMMENT { result = CommentNode.new(val[0]) }
|
||||
;
|
||||
|
||||
# Arithmetic and logical operators
|
||||
# For Ruby's Operator precedence, see:
|
||||
# https://www.cs.auckland.ac.nz/references/ruby/ProgrammingRuby/language.html
|
||||
Operation:
|
||||
'!' Expression { result = OpNode.new(val[0], val[1]) }
|
||||
| '!!' Expression { result = OpNode.new(val[0], val[1]) }
|
||||
| '-' Expression = UMINUS { result = OpNode.new(val[0], val[1]) }
|
||||
| '+' Expression = UPLUS { result = OpNode.new(val[0], val[1]) }
|
||||
| NOT Expression { result = OpNode.new(val[0], val[1]) }
|
||||
| '~' Expression { result = OpNode.new(val[0], val[1]) }
|
||||
| '--' Expression { result = OpNode.new(val[0], val[1]) }
|
||||
| '++' Expression { result = OpNode.new(val[0], val[1]) }
|
||||
| DELETE Expression { result = OpNode.new(val[0], val[1]) }
|
||||
| TYPEOF Expression { result = OpNode.new(val[0], val[1]) }
|
||||
| Expression '--' { result = OpNode.new(val[1], val[0], nil, true) }
|
||||
| Expression '++' { result = OpNode.new(val[1], val[0], nil, true) }
|
||||
|
||||
| Expression '*' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '/' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '%' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
|
||||
| Expression '+' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '-' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
|
||||
| Expression '<<' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '>>' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '>>>' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
|
||||
| Expression '&' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '|' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '^' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
|
||||
| Expression '<=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '<' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '>' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '>=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
|
||||
| Expression '==' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '!=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression IS Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression ISNT Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
|
||||
| Expression '&&' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '||' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression AND Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression OR Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '?' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
|
||||
| Expression '-=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '+=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '/=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '*=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '%=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '||=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '&&=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression '?=' Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
|
||||
| Expression INSTANCEOF Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
| Expression IN Expression { result = OpNode.new(val[1], val[0], val[2]) }
|
||||
;
|
||||
|
||||
# The existence operator.
|
||||
Existence:
|
||||
Expression '?' { result = ExistenceNode.new(val[0]) }
|
||||
;
|
||||
|
||||
# Function definition.
|
||||
Code:
|
||||
PARAM_START ParamList PARAM_END
|
||||
FuncGlyph Block { result = CodeNode.new(val[1], val[4], val[3]) }
|
||||
| FuncGlyph Block { result = CodeNode.new([], val[1], val[0]) }
|
||||
;
|
||||
|
||||
# The symbols to signify functions, and bound functions.
|
||||
FuncGlyph:
|
||||
'->' { result = :func }
|
||||
| '=>' { result = :boundfunc }
|
||||
;
|
||||
|
||||
# The parameters to a function definition.
|
||||
ParamList:
|
||||
Param { result = val }
|
||||
| ParamList "," Param { result = val[0] << val[2] }
|
||||
;
|
||||
|
||||
# A Parameter (or ParamSplat) in a function definition.
|
||||
Param:
|
||||
PARAM
|
||||
| PARAM "." "." "." { result = SplatNode.new(val[0]) }
|
||||
;
|
||||
|
||||
# A regular splat.
|
||||
Splat:
|
||||
Expression "." "." "." { result = SplatNode.new(val[0]) }
|
||||
;
|
||||
|
||||
# Expressions that can be treated as values.
|
||||
Value:
|
||||
IDENTIFIER { result = ValueNode.new(val[0]) }
|
||||
| Literal { result = ValueNode.new(val[0]) }
|
||||
| Array { result = ValueNode.new(val[0]) }
|
||||
| Object { result = ValueNode.new(val[0]) }
|
||||
| Parenthetical { result = ValueNode.new(val[0]) }
|
||||
| Range { result = ValueNode.new(val[0]) }
|
||||
| This { result = ValueNode.new(val[0]) }
|
||||
| Value Accessor { result = val[0] << val[1] }
|
||||
| Invocation Accessor { result = ValueNode.new(val[0], [val[1]]) }
|
||||
;
|
||||
|
||||
# Accessing into an object or array, through dot or index notation.
|
||||
Accessor:
|
||||
PROPERTY_ACCESS IDENTIFIER { result = AccessorNode.new(val[1]) }
|
||||
| PROTOTYPE_ACCESS IDENTIFIER { result = AccessorNode.new(val[1], :prototype) }
|
||||
| SOAK_ACCESS IDENTIFIER { result = AccessorNode.new(val[1], :soak) }
|
||||
| Index { result = val[0] }
|
||||
| Slice { result = SliceNode.new(val[0]) }
|
||||
;
|
||||
|
||||
# Indexing into an object or array.
|
||||
Index:
|
||||
INDEX_START Expression INDEX_END { result = IndexNode.new(val[1]) }
|
||||
;
|
||||
|
||||
# An object literal.
|
||||
Object:
|
||||
"{" AssignList "}" { result = ObjectNode.new(val[1]) }
|
||||
;
|
||||
|
||||
# Assignment within an object literal (comma or newline separated).
|
||||
AssignList:
|
||||
/* nothing */ { result = [] }
|
||||
| AssignObj { result = val }
|
||||
| AssignList "," AssignObj { result = val[0] << val[2] }
|
||||
| AssignList Terminator AssignObj { result = val[0] << val[2] }
|
||||
| AssignList ","
|
||||
Terminator AssignObj { result = val[0] << val[3] }
|
||||
| INDENT AssignList OUTDENT { result = val[1] }
|
||||
;
|
||||
|
||||
# All flavors of function call (instantiation, super, and regular).
|
||||
Call:
|
||||
Invocation { result = val[0] }
|
||||
| NEW Invocation { result = val[1].new_instance }
|
||||
| Super { result = val[0] }
|
||||
;
|
||||
|
||||
# Extending an object's prototype.
|
||||
Extends:
|
||||
Value EXTENDS Value { result = ExtendsNode.new(val[0], val[2]) }
|
||||
;
|
||||
|
||||
# A generic function invocation.
|
||||
Invocation:
|
||||
Value Arguments { result = CallNode.new(val[0], val[1]) }
|
||||
| Invocation Arguments { result = CallNode.new(val[0], val[1]) }
|
||||
;
|
||||
|
||||
# The list of arguments to a function invocation.
|
||||
Arguments:
|
||||
CALL_START ArgList CALL_END { result = val[1] }
|
||||
;
|
||||
|
||||
# Calling super.
|
||||
Super:
|
||||
SUPER CALL_START ArgList CALL_END { result = CallNode.new(Value.new('super'), val[2]) }
|
||||
;
|
||||
|
||||
# This references, either naked or to a property.
|
||||
This:
|
||||
'@' { result = ThisNode.new }
|
||||
| '@' IDENTIFIER { result = ThisNode.new(val[1]) }
|
||||
;
|
||||
|
||||
# The range literal.
|
||||
Range:
|
||||
"[" Expression
|
||||
"." "." Expression "]" { result = RangeNode.new(val[1], val[4]) }
|
||||
| "[" Expression
|
||||
"." "." "." Expression "]" { result = RangeNode.new(val[1], val[5], true) }
|
||||
;
|
||||
|
||||
# The slice literal.
|
||||
Slice:
|
||||
INDEX_START Expression "." "."
|
||||
Expression INDEX_END { result = RangeNode.new(val[1], val[4]) }
|
||||
| INDEX_START Expression "." "." "."
|
||||
Expression INDEX_END { result = RangeNode.new(val[1], val[5], true) }
|
||||
;
|
||||
|
||||
# The array literal.
|
||||
Array:
|
||||
"[" ArgList "]" { result = ArrayNode.new(val[1]) }
|
||||
;
|
||||
|
||||
# A list of arguments to a method call, or as the contents of an array.
|
||||
ArgList:
|
||||
/* nothing */ { result = [] }
|
||||
| Expression { result = val }
|
||||
| INDENT Expression { result = [val[1]] }
|
||||
| ArgList "," Expression { result = val[0] << val[2] }
|
||||
| ArgList Terminator Expression { result = val[0] << val[2] }
|
||||
| ArgList "," Terminator Expression { result = val[0] << val[3] }
|
||||
| ArgList "," INDENT Expression { result = val[0] << val[3] }
|
||||
| ArgList OUTDENT { result = val[0] }
|
||||
;
|
||||
|
||||
# Just simple, comma-separated, required arguments (no fancy syntax).
|
||||
SimpleArgs:
|
||||
Expression { result = val[0] }
|
||||
| SimpleArgs "," Expression { result = ([val[0]] << val[2]).flatten }
|
||||
;
|
||||
|
||||
# Try/catch/finally exception handling blocks.
|
||||
Try:
|
||||
TRY Block Catch { result = TryNode.new(val[1], val[2][0], val[2][1]) }
|
||||
| TRY Block FINALLY Block { result = TryNode.new(val[1], nil, nil, val[3]) }
|
||||
| TRY Block Catch
|
||||
FINALLY Block { result = TryNode.new(val[1], val[2][0], val[2][1], val[4]) }
|
||||
;
|
||||
|
||||
# A catch clause.
|
||||
Catch:
|
||||
CATCH IDENTIFIER Block { result = [val[1], val[2]] }
|
||||
;
|
||||
|
||||
# Throw an exception.
|
||||
Throw:
|
||||
THROW Expression { result = ThrowNode.new(val[1]) }
|
||||
;
|
||||
|
||||
# Parenthetical expressions.
|
||||
Parenthetical:
|
||||
"(" Expression ")" { result = ParentheticalNode.new(val[1], val[0].line) }
|
||||
;
|
||||
|
||||
# The while loop. (there is no do..while).
|
||||
While:
|
||||
WHILE Expression Block { result = WhileNode.new(val[1], val[2]) }
|
||||
| WHILE Expression { result = WhileNode.new(val[1], nil) }
|
||||
| Expression WHILE Expression { result = WhileNode.new(val[2], Expressions.wrap(val[0])) }
|
||||
;
|
||||
|
||||
# Array comprehensions, including guard and current index.
|
||||
# Looks a little confusing, check nodes.rb for the arguments to ForNode.
|
||||
For:
|
||||
Expression FOR
|
||||
ForVariables ForSource { result = ForNode.new(val[0], val[3], val[2][0], val[2][1]) }
|
||||
| FOR ForVariables ForSource Block { result = ForNode.new(val[3], val[2], val[1][0], val[1][1]) }
|
||||
;
|
||||
|
||||
# An array comprehension has variables for the current element and index.
|
||||
ForVariables:
|
||||
IDENTIFIER { result = val }
|
||||
| IDENTIFIER "," IDENTIFIER { result = [val[0], val[2]] }
|
||||
;
|
||||
|
||||
# The source of the array comprehension can optionally be filtered.
|
||||
ForSource:
|
||||
IN Expression { result = {:source => val[1]} }
|
||||
| OF Expression { result = {:source => val[1], :object => true} }
|
||||
| ForSource
|
||||
WHEN Expression { result = val[0].merge(:filter => val[2]) }
|
||||
| ForSource
|
||||
BY Expression { result = val[0].merge(:step => val[2]) }
|
||||
;
|
||||
|
||||
# Switch/When blocks.
|
||||
Switch:
|
||||
SWITCH Expression INDENT
|
||||
Whens OUTDENT { result = val[3].rewrite_condition(val[1]) }
|
||||
| SWITCH Expression INDENT
|
||||
Whens ELSE Block OUTDENT { result = val[3].rewrite_condition(val[1]).add_else(val[5]) }
|
||||
;
|
||||
|
||||
# The inner list of whens.
|
||||
Whens:
|
||||
When { result = val[0] }
|
||||
| Whens When { result = val[0] << val[1] }
|
||||
;
|
||||
|
||||
# An individual when.
|
||||
When:
|
||||
LEADING_WHEN SimpleArgs Block { result = IfNode.new(val[1], val[2], nil, {:statement => true}) }
|
||||
| LEADING_WHEN SimpleArgs Block
|
||||
Terminator { result = IfNode.new(val[1], val[2], nil, {:statement => true}) }
|
||||
| Comment Terminator When { result = val[2].add_comment(val[0]) }
|
||||
;
|
||||
|
||||
# The most basic form of "if".
|
||||
IfBlock:
|
||||
IF Expression Block { result = IfNode.new(val[1], val[2]) }
|
||||
;
|
||||
|
||||
# An elsif portion of an if-else block.
|
||||
ElsIf:
|
||||
ELSE IfBlock { result = val[1].force_statement }
|
||||
;
|
||||
|
||||
# Multiple elsifs can be chained together.
|
||||
ElsIfs:
|
||||
ElsIf { result = val[0] }
|
||||
| ElsIfs ElsIf { result = val[0].add_else(val[1]) }
|
||||
;
|
||||
|
||||
# Terminating else bodies are strictly optional.
|
||||
ElseBody
|
||||
/* nothing */ { result = nil }
|
||||
| ELSE Block { result = val[1] }
|
||||
;
|
||||
|
||||
# All the alternatives for ending an if-else block.
|
||||
IfEnd:
|
||||
ElseBody { result = val[0] }
|
||||
| ElsIfs ElseBody { result = val[0].add_else(val[1]) }
|
||||
;
|
||||
|
||||
# The full complement of if blocks, including postfix one-liner ifs and unlesses.
|
||||
If:
|
||||
IfBlock IfEnd { result = val[0].add_else(val[1]) }
|
||||
| Expression IF Expression { result = IfNode.new(val[2], Expressions.wrap(val[0]), nil, {:statement => true}) }
|
||||
| Expression UNLESS Expression { result = IfNode.new(val[2], Expressions.wrap(val[0]), nil, {:statement => true, :invert => true}) }
|
||||
;
|
||||
|
||||
end
|
||||
|
||||
---- header
|
||||
module CoffeeScript
|
||||
|
||||
---- inner
|
||||
# Lex and parse a CoffeeScript.
|
||||
def parse(code)
|
||||
# Uncomment the following line to enable grammar debugging, in combination
|
||||
# with the -g flag in the Rake build task.
|
||||
# @yydebug = true
|
||||
@tokens = Lexer.new.tokenize(code)
|
||||
do_parse
|
||||
end
|
||||
|
||||
# Retrieve the next token from the list.
|
||||
def next_token
|
||||
@tokens.shift
|
||||
end
|
||||
|
||||
# Raise a custom error class that knows about line numbers.
|
||||
def on_error(error_token_id, error_value, value_stack)
|
||||
raise ParseError.new(token_to_str(error_token_id), error_value, value_stack)
|
||||
end
|
||||
|
||||
---- footer
|
||||
end
|
|
@ -1,273 +0,0 @@
|
|||
module CoffeeScript
|
||||
|
||||
# The lexer reads a stream of CoffeeScript and divvys it up into tagged
|
||||
# tokens. A minor bit of the ambiguity in the grammar has been avoided by
|
||||
# pushing some extra smarts into the Lexer.
|
||||
class Lexer
|
||||
|
||||
# The list of keywords passed verbatim to the parser.
|
||||
KEYWORDS = ["if", "else", "then", "unless",
|
||||
"true", "false", "yes", "no", "on", "off",
|
||||
"and", "or", "is", "isnt", "not",
|
||||
"new", "return",
|
||||
"try", "catch", "finally", "throw",
|
||||
"break", "continue",
|
||||
"for", "in", "of", "by", "where", "while",
|
||||
"delete", "instanceof", "typeof",
|
||||
"switch", "when",
|
||||
"super", "extends"]
|
||||
|
||||
# Token matching regexes.
|
||||
IDENTIFIER = /\A([a-zA-Z$_](\w|\$)*)/
|
||||
NUMBER = /\A(\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\.[0-9]+)?(e[+\-]?[0-9]+)?)))\b/i
|
||||
STRING = /\A(""|''|"(.*?)([^\\]|\\\\)"|'(.*?)([^\\]|\\\\)')/m
|
||||
HEREDOC = /\A("{6}|'{6}|"{3}\n?(.*?)\n?([ \t]*)"{3}|'{3}\n?(.*?)\n?([ \t]*)'{3})/m
|
||||
JS = /\A(``|`(.*?)([^\\]|\\\\)`)/m
|
||||
OPERATOR = /\A([+\*&|\/\-%=<>:!?]+)/
|
||||
WHITESPACE = /\A([ \t]+)/
|
||||
COMMENT = /\A(((\n?[ \t]*)?#.*$)+)/
|
||||
CODE = /\A((-|=)>)/
|
||||
REGEX = /\A(\/(.*?)([^\\]|\\\\)\/[imgy]{0,4})/
|
||||
MULTI_DENT = /\A((\n([ \t]*))+)(\.)?/
|
||||
LAST_DENT = /\n([ \t]*)/
|
||||
ASSIGNMENT = /\A(:|=)\Z/
|
||||
|
||||
# Token cleaning regexes.
|
||||
JS_CLEANER = /(\A`|`\Z)/
|
||||
MULTILINER = /\n/
|
||||
STRING_NEWLINES = /\n[ \t]*/
|
||||
COMMENT_CLEANER = /(^[ \t]*#|\n[ \t]*$)/
|
||||
NO_NEWLINE = /\A([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)\Z/
|
||||
HEREDOC_INDENT = /^[ \t]+/
|
||||
|
||||
# Tokens which a regular expression will never immediately follow, but which
|
||||
# a division operator might.
|
||||
# See: http://www.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions
|
||||
NOT_REGEX = [
|
||||
:IDENTIFIER, :NUMBER, :REGEX, :STRING,
|
||||
')', '++', '--', ']', '}',
|
||||
:FALSE, :NULL, :TRUE
|
||||
]
|
||||
|
||||
# Tokens which could legitimately be invoked or indexed.
|
||||
CALLABLE = [:IDENTIFIER, :SUPER, ')', ']', '}', :STRING]
|
||||
|
||||
# Scan by attempting to match tokens one character at a time. Slow and steady.
|
||||
def tokenize(code)
|
||||
@code = code.chomp # Cleanup code by remove extra line breaks
|
||||
@i = 0 # Current character position we're parsing
|
||||
@line = 1 # The current line.
|
||||
@indent = 0 # The current indent level.
|
||||
@indents = [] # The stack of all indent levels we are currently within.
|
||||
@tokens = [] # Collection of all parsed tokens in the form [:TOKEN_TYPE, value]
|
||||
@spaced = nil # The last value that has a space following it.
|
||||
while @i < @code.length
|
||||
@chunk = @code[@i..-1]
|
||||
extract_next_token
|
||||
end
|
||||
puts "original stream: #{@tokens.inspect}" if ENV['VERBOSE']
|
||||
close_indentation
|
||||
Rewriter.new.rewrite(@tokens)
|
||||
end
|
||||
|
||||
# At every position, run through this list of attempted matches,
|
||||
# short-circuiting if any of them succeed.
|
||||
def extract_next_token
|
||||
return if identifier_token
|
||||
return if number_token
|
||||
return if heredoc_token
|
||||
return if string_token
|
||||
return if js_token
|
||||
return if regex_token
|
||||
return if indent_token
|
||||
return if comment_token
|
||||
return if whitespace_token
|
||||
return literal_token
|
||||
end
|
||||
|
||||
# Tokenizers ==========================================================
|
||||
|
||||
# Matches identifying literals: variables, keywords, method names, etc.
|
||||
def identifier_token
|
||||
return false unless identifier = @chunk[IDENTIFIER, 1]
|
||||
# Keywords are special identifiers tagged with their own name,
|
||||
# 'if' will result in an [:IF, "if"] token.
|
||||
tag = KEYWORDS.include?(identifier) ? identifier.upcase.to_sym : :IDENTIFIER
|
||||
tag = :LEADING_WHEN if tag == :WHEN && [:OUTDENT, :INDENT, "\n"].include?(last_tag)
|
||||
@tokens[-1][0] = :PROTOTYPE_ACCESS if tag == :IDENTIFIER && last_value == '::'
|
||||
if tag == :IDENTIFIER && last_value == '.' && !(@tokens[-2] && @tokens[-2][1] == '.')
|
||||
if @tokens[-2][0] == "?"
|
||||
@tokens[-1][0] = :SOAK_ACCESS
|
||||
@tokens.delete_at(-2)
|
||||
else
|
||||
@tokens[-1][0] = :PROPERTY_ACCESS
|
||||
end
|
||||
end
|
||||
token(tag, identifier)
|
||||
@i += identifier.length
|
||||
end
|
||||
|
||||
# Matches numbers, including decimals, hex, and exponential notation.
|
||||
def number_token
|
||||
return false unless number = @chunk[NUMBER, 1]
|
||||
token(:NUMBER, number)
|
||||
@i += number.length
|
||||
end
|
||||
|
||||
# Matches strings, including multi-line strings.
|
||||
def string_token
|
||||
return false unless string = @chunk[STRING, 1]
|
||||
escaped = string.gsub(STRING_NEWLINES, " \\\n")
|
||||
token(:STRING, escaped)
|
||||
@line += string.count("\n")
|
||||
@i += string.length
|
||||
end
|
||||
|
||||
# Matches heredocs, adjusting indentation to the correct level.
|
||||
def heredoc_token
|
||||
return false unless match = @chunk.match(HEREDOC)
|
||||
doc = match[2] || match[4]
|
||||
indent = doc.scan(HEREDOC_INDENT).min
|
||||
doc.gsub!(/^#{indent}/, "")
|
||||
doc.gsub!("\n", "\\n")
|
||||
doc.gsub!('"', '\\"')
|
||||
token(:STRING, "\"#{doc}\"")
|
||||
@line += match[1].count("\n")
|
||||
@i += match[1].length
|
||||
end
|
||||
|
||||
# Matches interpolated JavaScript.
|
||||
def js_token
|
||||
return false unless script = @chunk[JS, 1]
|
||||
token(:JS, script.gsub(JS_CLEANER, ''))
|
||||
@i += script.length
|
||||
end
|
||||
|
||||
# Matches regular expression literals.
|
||||
def regex_token
|
||||
return false unless regex = @chunk[REGEX, 1]
|
||||
return false if NOT_REGEX.include?(last_tag)
|
||||
token(:REGEX, regex)
|
||||
@i += regex.length
|
||||
end
|
||||
|
||||
# Matches and consumes comments.
|
||||
def comment_token
|
||||
return false unless comment = @chunk[COMMENT, 1]
|
||||
@line += comment.scan(MULTILINER).length
|
||||
token(:COMMENT, comment.gsub(COMMENT_CLEANER, '').split(MULTILINER))
|
||||
token("\n", "\n")
|
||||
@i += comment.length
|
||||
end
|
||||
|
||||
# Record tokens for indentation differing from the previous line.
|
||||
def indent_token
|
||||
return false unless indent = @chunk[MULTI_DENT, 1]
|
||||
@line += indent.scan(MULTILINER).size
|
||||
@i += indent.size
|
||||
next_character = @chunk[MULTI_DENT, 4]
|
||||
prev = @tokens[-2]
|
||||
no_newlines = next_character == '.' || (last_value.to_s.match(NO_NEWLINE) && prev && prev[0] != '.' && !last_value.match(CODE))
|
||||
return suppress_newlines(indent) if no_newlines
|
||||
size = indent.scan(LAST_DENT).last.last.length
|
||||
return newline_token(indent) if size == @indent
|
||||
if size > @indent
|
||||
token(:INDENT, size - @indent)
|
||||
@indents << (size - @indent)
|
||||
else
|
||||
outdent_token(@indent - size)
|
||||
end
|
||||
@indent = size
|
||||
end
|
||||
|
||||
# Record an oudent token or tokens, if we're moving back inwards past
|
||||
# multiple recorded indents.
|
||||
def outdent_token(move_out)
|
||||
while move_out > 0 && !@indents.empty?
|
||||
last_indent = @indents.pop
|
||||
token(:OUTDENT, last_indent)
|
||||
move_out -= last_indent
|
||||
end
|
||||
token("\n", "\n")
|
||||
end
|
||||
|
||||
# Matches and consumes non-meaningful whitespace.
|
||||
def whitespace_token
|
||||
return false unless whitespace = @chunk[WHITESPACE, 1]
|
||||
@spaced = last_value
|
||||
@i += whitespace.length
|
||||
end
|
||||
|
||||
# Multiple newlines get merged together.
|
||||
# Use a trailing \ to escape newlines.
|
||||
def newline_token(newlines)
|
||||
token("\n", "\n") unless last_value == "\n"
|
||||
true
|
||||
end
|
||||
|
||||
# Tokens to explicitly escape newlines are removed once their job is done.
|
||||
def suppress_newlines(newlines)
|
||||
@tokens.pop if last_value == "\\"
|
||||
true
|
||||
end
|
||||
|
||||
# We treat all other single characters as a token. Eg.: ( ) , . !
|
||||
# Multi-character operators are also literal tokens, so that Racc can assign
|
||||
# the proper order of operations.
|
||||
def literal_token
|
||||
value = @chunk[OPERATOR, 1]
|
||||
tag_parameters if value && value.match(CODE)
|
||||
value ||= @chunk[0,1]
|
||||
tag = value.match(ASSIGNMENT) ? :ASSIGN : value
|
||||
if !@spaced.equal?(last_value) && CALLABLE.include?(last_tag)
|
||||
tag = :CALL_START if value == '('
|
||||
tag = :INDEX_START if value == '['
|
||||
end
|
||||
token(tag, value)
|
||||
@i += value.length
|
||||
end
|
||||
|
||||
# Helpers ==========================================================
|
||||
|
||||
# Add a token to the results, taking note of the line number.
|
||||
def token(tag, value)
|
||||
@tokens << [tag, Value.new(value, @line)]
|
||||
end
|
||||
|
||||
# Peek at the previous token's value.
|
||||
def last_value
|
||||
@tokens.last && @tokens.last[1]
|
||||
end
|
||||
|
||||
# Peek at the previous token's tag.
|
||||
def last_tag
|
||||
@tokens.last && @tokens.last[0]
|
||||
end
|
||||
|
||||
# A source of ambiguity in our grammar was parameter lists in function
|
||||
# definitions (as opposed to argument lists in function calls). Tag
|
||||
# parameter identifiers in order to avoid this. Also, parameter lists can
|
||||
# make use of splats.
|
||||
def tag_parameters
|
||||
return if last_tag != ')'
|
||||
i = 0
|
||||
loop do
|
||||
i -= 1
|
||||
tok = @tokens[i]
|
||||
return if !tok
|
||||
case tok[0]
|
||||
when :IDENTIFIER then tok[0] = :PARAM
|
||||
when ')' then tok[0] = :PARAM_END
|
||||
when '(' then return tok[0] = :PARAM_START
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Close up all remaining open blocks. IF the first token is an indent,
|
||||
# axe it.
|
||||
def close_indentation
|
||||
outdent_token(@indent)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load Diff
|
@ -1,29 +0,0 @@
|
|||
module CoffeeScript
|
||||
|
||||
# Racc will raise this Exception whenever a syntax error occurs. The main
|
||||
# benefit over the Racc::ParseError is that the CoffeeScript::ParseError is
|
||||
# line-number aware.
|
||||
class ParseError < Racc::ParseError
|
||||
|
||||
TOKEN_MAP = {
|
||||
'INDENT' => 'indent',
|
||||
'OUTDENT' => 'outdent',
|
||||
"\n" => 'newline'
|
||||
}
|
||||
|
||||
def initialize(token_id, value, stack=nil, message=nil)
|
||||
@token_id, @value, @stack, @message = token_id, value, stack, message
|
||||
end
|
||||
|
||||
def message
|
||||
line = @value.respond_to?(:line) ? @value.line : "END"
|
||||
line_part = "line #{line}:"
|
||||
id_part = @token_id != @value.to_s ? " unexpected #{@token_id.to_s.downcase}" : ""
|
||||
val_part = @message || "for #{TOKEN_MAP[@value.to_s] || "'#{@value}'"}"
|
||||
"#{line_part} syntax error, #{val_part}#{id_part}"
|
||||
end
|
||||
alias_method :inspect, :message
|
||||
|
||||
end
|
||||
|
||||
end
|
File diff suppressed because it is too large
Load Diff
|
@ -1,289 +0,0 @@
|
|||
module CoffeeScript
|
||||
|
||||
# In order to keep the grammar simple, the stream of tokens that the Lexer
|
||||
# emits is rewritten by the Rewriter, smoothing out ambiguities, mis-nested
|
||||
# indentation, and single-line flavors of expressions.
|
||||
class Rewriter
|
||||
|
||||
# Tokens that must be balanced.
|
||||
BALANCED_PAIRS = [['(', ')'], ['[', ']'], ['{', '}'], [:INDENT, :OUTDENT],
|
||||
[:PARAM_START, :PARAM_END], [:CALL_START, :CALL_END], [:INDEX_START, :INDEX_END]]
|
||||
|
||||
# Tokens that signal the start of a balanced pair.
|
||||
EXPRESSION_START = BALANCED_PAIRS.map {|pair| pair.first }
|
||||
|
||||
# Tokens that signal the end of a balanced pair.
|
||||
EXPRESSION_TAIL = BALANCED_PAIRS.map {|pair| pair.last }
|
||||
|
||||
# Tokens that indicate the close of a clause of an expression.
|
||||
EXPRESSION_CLOSE = [:CATCH, :WHEN, :ELSE, :FINALLY] + EXPRESSION_TAIL
|
||||
|
||||
# Tokens pairs that, in immediate succession, indicate an implicit call.
|
||||
IMPLICIT_FUNC = [:IDENTIFIER, :SUPER, ')', :CALL_END, ']', :INDEX_END]
|
||||
IMPLICIT_END = [:IF, :UNLESS, :FOR, :WHILE, "\n", ';', :OUTDENT]
|
||||
IMPLICIT_CALL = [:IDENTIFIER, :NUMBER, :STRING, :JS, :REGEX, :NEW, :PARAM_START,
|
||||
:TRY, :DELETE, :TYPEOF, :SWITCH,
|
||||
:TRUE, :FALSE, :YES, :NO, :ON, :OFF, '!', '!!', :NOT,
|
||||
'@', '->', '=>', '[', '(', '{']
|
||||
|
||||
# The inverse mappings of token pairs we're trying to fix up.
|
||||
INVERSES = BALANCED_PAIRS.inject({}) do |memo, pair|
|
||||
memo[pair.first] = pair.last
|
||||
memo[pair.last] = pair.first
|
||||
memo
|
||||
end
|
||||
|
||||
# Single-line flavors of block expressions that have unclosed endings.
|
||||
# The grammar can't disambiguate them, so we insert the implicit indentation.
|
||||
SINGLE_LINERS = [:ELSE, "->", "=>", :TRY, :FINALLY, :THEN]
|
||||
SINGLE_CLOSERS = ["\n", :CATCH, :FINALLY, :ELSE, :OUTDENT, :LEADING_WHEN, :PARAM_START]
|
||||
|
||||
# Rewrite the token stream in multiple passes, one logical filter at
|
||||
# a time. This could certainly be changed into a single pass through the
|
||||
# stream, with a big ol' efficient switch, but it's much nicer like this.
|
||||
def rewrite(tokens)
|
||||
@tokens = tokens
|
||||
adjust_comments
|
||||
remove_leading_newlines
|
||||
remove_mid_expression_newlines
|
||||
move_commas_outside_outdents
|
||||
close_open_calls_and_indexes
|
||||
add_implicit_parentheses
|
||||
add_implicit_indentation
|
||||
ensure_balance(*BALANCED_PAIRS)
|
||||
rewrite_closing_parens
|
||||
@tokens
|
||||
end
|
||||
|
||||
# Rewrite the token stream, looking one token ahead and behind.
|
||||
# Allow the return value of the block to tell us how many tokens to move
|
||||
# forwards (or backwards) in the stream, to make sure we don't miss anything
|
||||
# as the stream changes length under our feet.
|
||||
def scan_tokens
|
||||
i = 0
|
||||
loop do
|
||||
break unless @tokens[i]
|
||||
move = yield(@tokens[i - 1], @tokens[i], @tokens[i + 1], i)
|
||||
i += move
|
||||
end
|
||||
end
|
||||
|
||||
# Massage newlines and indentations so that comments don't have to be
|
||||
# correctly indented, or appear on their own line.
|
||||
def adjust_comments
|
||||
scan_tokens do |prev, token, post, i|
|
||||
next 1 unless token[0] == :COMMENT
|
||||
before, after = @tokens[i - 2], @tokens[i + 2]
|
||||
if before && after &&
|
||||
((before[0] == :INDENT && after[0] == :OUTDENT) ||
|
||||
(before[0] == :OUTDENT && after[0] == :INDENT)) &&
|
||||
before[1] == after[1]
|
||||
@tokens.delete_at(i + 2)
|
||||
@tokens.delete_at(i - 2)
|
||||
next 0
|
||||
elsif prev[0] == "\n" && [:INDENT].include?(after[0])
|
||||
@tokens.delete_at(i + 2)
|
||||
@tokens[i - 1] = after
|
||||
next 1
|
||||
elsif !["\n", :INDENT, :OUTDENT].include?(prev[0])
|
||||
@tokens.insert(i, ["\n", Value.new("\n", token[1].line)])
|
||||
next 2
|
||||
else
|
||||
next 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Leading newlines would introduce an ambiguity in the grammar, so we
|
||||
# dispatch them here.
|
||||
def remove_leading_newlines
|
||||
@tokens.shift if @tokens[0][0] == "\n"
|
||||
end
|
||||
|
||||
# Some blocks occur in the middle of expressions -- when we're expecting
|
||||
# this, remove their trailing newlines.
|
||||
def remove_mid_expression_newlines
|
||||
scan_tokens do |prev, token, post, i|
|
||||
next 1 unless post && EXPRESSION_CLOSE.include?(post[0]) && token[0] == "\n"
|
||||
@tokens.delete_at(i)
|
||||
next 0
|
||||
end
|
||||
end
|
||||
|
||||
# Make sure that we don't accidentally break trailing commas, which need
|
||||
# to go on the outside of expression closers.
|
||||
def move_commas_outside_outdents
|
||||
scan_tokens do |prev, token, post, i|
|
||||
if token[0] == :OUTDENT && prev[0] == ','
|
||||
@tokens.delete_at(i)
|
||||
@tokens.insert(i - 1, token)
|
||||
end
|
||||
next 1
|
||||
end
|
||||
end
|
||||
|
||||
# We've tagged the opening parenthesis of a method call, and the opening
|
||||
# bracket of an indexing operation. Match them with their close.
|
||||
def close_open_calls_and_indexes
|
||||
parens, brackets = [0], [0]
|
||||
scan_tokens do |prev, token, post, i|
|
||||
case token[0]
|
||||
when :CALL_START then parens.push(0)
|
||||
when :INDEX_START then brackets.push(0)
|
||||
when '(' then parens[-1] += 1
|
||||
when '[' then brackets[-1] += 1
|
||||
when ')'
|
||||
if parens.last == 0
|
||||
parens.pop
|
||||
token[0] = :CALL_END
|
||||
else
|
||||
parens[-1] -= 1
|
||||
end
|
||||
when ']'
|
||||
if brackets.last == 0
|
||||
brackets.pop
|
||||
token[0] = :INDEX_END
|
||||
else
|
||||
brackets[-1] -= 1
|
||||
end
|
||||
end
|
||||
next 1
|
||||
end
|
||||
end
|
||||
|
||||
# Methods may be optionally called without parentheses, for simple cases.
|
||||
# Insert the implicit parentheses here, so that the parser doesn't have to
|
||||
# deal with them.
|
||||
def add_implicit_parentheses
|
||||
stack = [0]
|
||||
scan_tokens do |prev, token, post, i|
|
||||
stack.push(0) if token[0] == :INDENT
|
||||
if token[0] == :OUTDENT
|
||||
last = stack.pop
|
||||
stack[-1] += last
|
||||
end
|
||||
if stack.last > 0 && (IMPLICIT_END.include?(token[0]) || post.nil?)
|
||||
idx = token[0] == :OUTDENT ? i + 1 : i
|
||||
stack.last.times { @tokens.insert(idx, [:CALL_END, Value.new(')', token[1].line)]) }
|
||||
size, stack[-1] = stack[-1] + 1, 0
|
||||
next size
|
||||
end
|
||||
next 1 unless IMPLICIT_FUNC.include?(prev[0]) && IMPLICIT_CALL.include?(token[0])
|
||||
@tokens.insert(i, [:CALL_START, Value.new('(', token[1].line)])
|
||||
stack[-1] += 1
|
||||
next 2
|
||||
end
|
||||
end
|
||||
|
||||
# Because our grammar is LALR(1), it can't handle some single-line
|
||||
# expressions that lack ending delimiters. Use the lexer to add the implicit
|
||||
# blocks, so it doesn't need to.
|
||||
# ')' can close a single-line block, but we need to make sure it's balanced.
|
||||
def add_implicit_indentation
|
||||
scan_tokens do |prev, token, post, i|
|
||||
next 1 unless SINGLE_LINERS.include?(token[0]) && post[0] != :INDENT &&
|
||||
!(token[0] == :ELSE && post[0] == :IF) # Elsifs shouldn't get blocks.
|
||||
starter = token[0]
|
||||
line = token[1].line
|
||||
@tokens.insert(i + 1, [:INDENT, Value.new(2, line)])
|
||||
idx = i + 1
|
||||
parens = 0
|
||||
loop do
|
||||
idx += 1
|
||||
tok = @tokens[idx]
|
||||
if (!tok || SINGLE_CLOSERS.include?(tok[0]) ||
|
||||
(tok[0] == ')' && parens == 0)) &&
|
||||
!(starter == :ELSE && tok[0] == :ELSE)
|
||||
insertion = @tokens[idx - 1][0] == "," ? idx - 1 : idx
|
||||
@tokens.insert(insertion, [:OUTDENT, Value.new(2, line)])
|
||||
break
|
||||
end
|
||||
parens += 1 if tok[0] == '('
|
||||
parens -= 1 if tok[0] == ')'
|
||||
end
|
||||
next 1 unless token[0] == :THEN
|
||||
@tokens.delete_at(i)
|
||||
next 0
|
||||
end
|
||||
end
|
||||
|
||||
# Ensure that all listed pairs of tokens are correctly balanced throughout
|
||||
# the course of the token stream.
|
||||
def ensure_balance(*pairs)
|
||||
puts "\nbefore ensure_balance: #{@tokens.inspect}" if ENV['VERBOSE']
|
||||
levels, lines = Hash.new(0), Hash.new
|
||||
scan_tokens do |prev, token, post, i|
|
||||
pairs.each do |pair|
|
||||
open, close = *pair
|
||||
levels[open] += 1 if token[0] == open
|
||||
levels[open] -= 1 if token[0] == close
|
||||
lines[token[0]] = token[1].line
|
||||
raise ParseError.new(token[0], token[1], nil) if levels[open] < 0
|
||||
end
|
||||
next 1
|
||||
end
|
||||
unclosed = levels.detect {|k, v| v > 0 }
|
||||
sym = unclosed && unclosed[0]
|
||||
raise ParseError.new(sym, Value.new(sym, lines[sym]), nil, "unclosed '#{sym}'") if unclosed
|
||||
end
|
||||
|
||||
# We'd like to support syntax like this:
|
||||
# el.click((event) ->
|
||||
# el.hide())
|
||||
# In order to accomplish this, move outdents that follow closing parens
|
||||
# inwards, safely. The steps to accomplish this are:
|
||||
#
|
||||
# 1. Check that all paired tokens are balanced and in order.
|
||||
# 2. Rewrite the stream with a stack: if you see an '(' or INDENT, add it
|
||||
# to the stack. If you see an ')' or OUTDENT, pop the stack and replace
|
||||
# it with the inverse of what we've just popped.
|
||||
# 3. Keep track of "debt" for tokens that we fake, to make sure we end
|
||||
# up balanced in the end.
|
||||
#
|
||||
def rewrite_closing_parens
|
||||
verbose = ENV['VERBOSE']
|
||||
stack, debt = [], Hash.new(0)
|
||||
stack_stats = lambda { "stack: #{stack.inspect} debt: #{debt.inspect}\n\n" }
|
||||
puts "rewrite_closing_original: #{@tokens.inspect}" if verbose
|
||||
scan_tokens do |prev, token, post, i|
|
||||
tag, inv = token[0], INVERSES[token[0]]
|
||||
# Push openers onto the stack.
|
||||
if EXPRESSION_START.include?(tag)
|
||||
stack.push(token)
|
||||
puts "pushing #{tag} #{stack_stats[]}" if verbose
|
||||
next 1
|
||||
# The end of an expression, check stack and debt for a pair.
|
||||
elsif EXPRESSION_TAIL.include?(tag)
|
||||
puts @tokens[i..-1].inspect if verbose
|
||||
# If the tag is already in our debt, swallow it.
|
||||
if debt[inv] > 0
|
||||
debt[inv] -= 1
|
||||
@tokens.delete_at(i)
|
||||
puts "tag in debt #{tag} #{stack_stats[]}" if verbose
|
||||
next 0
|
||||
else
|
||||
# Pop the stack of open delimiters.
|
||||
match = stack.pop
|
||||
mtag = match[0]
|
||||
# Continue onwards if it's the expected tag.
|
||||
if tag == INVERSES[mtag]
|
||||
puts "expected tag #{tag} #{stack_stats[]}" if verbose
|
||||
next 1
|
||||
else
|
||||
# Unexpected close, insert correct close, adding to the debt.
|
||||
debt[mtag] += 1
|
||||
puts "unexpected #{tag}, replacing with #{INVERSES[mtag]} #{stack_stats[]}" if verbose
|
||||
val = mtag == :INDENT ? match[1] : INVERSES[mtag]
|
||||
@tokens.insert(i, [INVERSES[mtag], Value.new(val, token[1].line)])
|
||||
next 1
|
||||
end
|
||||
end
|
||||
else
|
||||
# Uninteresting token:
|
||||
next 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,95 +0,0 @@
|
|||
module CoffeeScript
|
||||
|
||||
# Scope objects form a tree corresponding to the shape of the function
|
||||
# definitions present in the script. They provide lexical scope, to determine
|
||||
# whether a variable has been seen before or if it needs to be declared.
|
||||
class Scope
|
||||
|
||||
attr_reader :parent, :expressions, :function, :variables, :temp_variable
|
||||
|
||||
# Initialize a scope with its parent, for lookups up the chain,
|
||||
# as well as the Expressions body where it should declare its variables,
|
||||
# and the function that it wraps.
|
||||
def initialize(parent, expressions, function)
|
||||
@parent, @expressions, @function = parent, expressions, function
|
||||
@variables = {}
|
||||
@temp_variable = @parent ? @parent.temp_variable.dup : '__a'
|
||||
end
|
||||
|
||||
# Look up a variable in lexical scope, or declare it if not found.
|
||||
def find(name, remote=false)
|
||||
found = check(name)
|
||||
return found if found || remote
|
||||
@variables[name.to_sym] = :var
|
||||
found
|
||||
end
|
||||
|
||||
# Define a local variable as originating from a parameter in current scope
|
||||
# -- no var required.
|
||||
def parameter(name)
|
||||
@variables[name.to_sym] = :param
|
||||
end
|
||||
|
||||
# Just check to see if a variable has already been declared.
|
||||
def check(name)
|
||||
return true if @variables[name.to_sym]
|
||||
!!(@parent && @parent.check(name))
|
||||
end
|
||||
|
||||
# You can reset a found variable on the immediate scope.
|
||||
def reset(name)
|
||||
@variables[name.to_sym] = false
|
||||
end
|
||||
|
||||
# Find an available, short, name for a compiler-generated variable.
|
||||
def free_variable
|
||||
@temp_variable.succ! while check(@temp_variable)
|
||||
@variables[@temp_variable.to_sym] = :var
|
||||
Value.new(@temp_variable.dup)
|
||||
end
|
||||
|
||||
# Ensure that an assignment is made at the top of scope (or top-level
|
||||
# scope, if requested).
|
||||
def assign(name, value, top=false)
|
||||
return @parent.assign(name, value, top) if top && @parent
|
||||
@variables[name.to_sym] = Value.new(value)
|
||||
end
|
||||
|
||||
# Does this scope reference any variables that need to be declared in the
|
||||
# given function body?
|
||||
def declarations?(body)
|
||||
!declared_variables.empty? && body == @expressions
|
||||
end
|
||||
|
||||
# Does this scope reference any assignments that need to be declared at the
|
||||
# top of the given function body?
|
||||
def assignments?(body)
|
||||
!assigned_variables.empty? && body == @expressions
|
||||
end
|
||||
|
||||
# Return the list of variables first declared in current scope.
|
||||
def declared_variables
|
||||
@variables.select {|k, v| v == :var }.map {|pair| pair[0].to_s }.sort
|
||||
end
|
||||
|
||||
# Return the list of variables that are supposed to be assigned at the top
|
||||
# of scope.
|
||||
def assigned_variables
|
||||
@variables.select {|k, v| v.is_a?(Value) }.sort_by {|pair| pair[0].to_s }
|
||||
end
|
||||
|
||||
def compiled_declarations
|
||||
declared_variables.join(', ')
|
||||
end
|
||||
|
||||
def compiled_assignments
|
||||
assigned_variables.map {|name, val| "#{name} = #{val}"}.join(', ')
|
||||
end
|
||||
|
||||
def inspect
|
||||
"<Scope:#{__id__} #{@variables.inspect}>"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -1,64 +0,0 @@
|
|||
module CoffeeScript
|
||||
|
||||
# Instead of producing raw Ruby objects, the Lexer produces values of this
|
||||
# class, wrapping native objects tagged with line number information.
|
||||
# Values masquerade as both strings and nodes -- being used both as nodes in
|
||||
# the AST, and as literally-interpolated values in the generated code.
|
||||
class Value
|
||||
attr_reader :value, :line
|
||||
|
||||
def initialize(value, line=nil)
|
||||
@value, @line = value, line
|
||||
end
|
||||
|
||||
def to_str
|
||||
@value.to_s
|
||||
end
|
||||
alias_method :to_s, :to_str
|
||||
|
||||
def to_sym
|
||||
to_str.to_sym
|
||||
end
|
||||
|
||||
def compile(o={})
|
||||
to_s
|
||||
end
|
||||
|
||||
def inspect
|
||||
@value.inspect
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
@value == other
|
||||
end
|
||||
|
||||
def [](index)
|
||||
@value[index]
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
@value.eql?(other)
|
||||
end
|
||||
|
||||
def hash
|
||||
@value.hash
|
||||
end
|
||||
|
||||
def match(regex)
|
||||
@value.match(regex)
|
||||
end
|
||||
|
||||
def children
|
||||
[]
|
||||
end
|
||||
|
||||
def statement_only?
|
||||
false
|
||||
end
|
||||
|
||||
def contains?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue