+CoffeeScript = require './coffee-script'
+CoffeeScript.require = require
+compile = CoffeeScript.compile
diff --git a/Cakefile b/Cakefile index 5a651f4779..3f7a58568a 100644 --- a/Cakefile +++ b/Cakefile @@ -85,20 +85,15 @@ task 'build:parser', 'rebuild the Jison parser (run build first)', -> fs.writeFile 'lib/coffee-script/parser.js', parser.generate() -task 'build:ultraviolet', 'build and install the Ultraviolet syntax highlighter', -> - exec 'plist2syntax ../coffee-script-tmbundle/Syntaxes/CoffeeScript.tmLanguage', (err) -> - throw err if err - exec 'sudo mv coffeescript.yaml /usr/local/lib/ruby/gems/1.8/gems/ultraviolet-0.10.2/syntax/coffeescript.syntax' - - task 'build:browser', 'rebuild the merged script for inclusion in the browser', -> code = '' for name in ['helpers', 'rewriter', 'lexer', 'parser', 'scope', 'nodes', 'sourcemap', 'coffee-script', 'browser'] code += """ - require['./#{name}'] = new function() { - var exports = this; + require['./#{name}'] = (function() { + var exports = {}, module = {exports: exports}; #{fs.readFileSync "lib/coffee-script/#{name}.js"} - }; + return module.exports; + })(); """ code = """ (function(root) { @@ -128,7 +123,7 @@ task 'doc:site', 'watch and continually rebuild the documentation for the websit task 'doc:source', 'rebuild the internal documentation', -> - exec 'docco src/*.coffee && cp -rf docs documentation && rm -r docs', (err) -> + exec 'docco src/*.*coffee && cp -rf docs documentation && rm -r docs', (err) -> throw err if err @@ -211,20 +206,15 @@ runTests = (CoffeeScript) -> log "failed #{failures.length} and #{message}", red for fail in failures {error, filename, description, source} = fail - jsFilename = filename.replace(/\.coffee$/,'.js') - match = error.stack?.match(new RegExp(fail.file+":(\\d+):(\\d+)")) - match = error.stack?.match(/on line (\d+):/) unless match - [match, line, col] = match if match console.log '' log " #{description}", red if description log " #{error.stack}", red - log " #{jsFilename}: line #{line ? 'unknown'}, column #{col ? 'unknown'}", red console.log " #{source}" if source return # Run every test in the `test` folder, recording failures. files = fs.readdirSync 'test' - for file in files when file.match /\.(lit)?coffee$/i + for file in files when helpers.isCoffee file literate = helpers.isLiterate file currentFile = filename = path.join 'test', file code = fs.readFileSync filename diff --git a/LICENSE b/LICENSE index dbe6b4e3b9..a396eaed7e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009-2012 Jeremy Ashkenas +Copyright (c) 2009-2013 Jeremy Ashkenas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation @@ -19,4 +19,4 @@ 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. \ No newline at end of file +OTHER DEALINGS IN THE SOFTWARE. diff --git a/documentation/coffee/block_comment.coffee b/documentation/coffee/block_comment.coffee index 431dfd9504..452ca3b03a 100644 --- a/documentation/coffee/block_comment.coffee +++ b/documentation/coffee/block_comment.coffee @@ -1,5 +1,5 @@ ### -CoffeeScript Compiler v1.6.1 +SkinnyMochaHalfCaffScript Compiler v1.0 Released under the MIT License ### diff --git a/documentation/coffee/constructor_destructuring.coffee b/documentation/coffee/constructor_destructuring.coffee new file mode 100644 index 0000000000..53c4b39a0d --- /dev/null +++ b/documentation/coffee/constructor_destructuring.coffee @@ -0,0 +1,4 @@ +class Person + constructor: (options) -> + {@name, @age, @height} = options + diff --git a/documentation/coffee/switch_with_no_expression.coffee b/documentation/coffee/switch_with_no_expression.coffee new file mode 100644 index 0000000000..08160199f9 --- /dev/null +++ b/documentation/coffee/switch_with_no_expression.coffee @@ -0,0 +1,8 @@ +score = 76 +grade = switch + when score < 60 then 'F' + when score < 70 then 'D' + when score < 80 then 'C' + when score < 90 then 'B' + else 'A' +# grade == 'C' diff --git a/documentation/docs/browser.html b/documentation/docs/browser.html index d5e9bbbfe3..7364615e24 100644 --- a/documentation/docs/browser.html +++ b/documentation/docs/browser.html @@ -1,43 +1,290 @@ -
browser.coffee | |
---|---|
Override exported methods for non-Node.js engines. | CoffeeScript = require './coffee-script'
-CoffeeScript.require = require |
Use standard JavaScript | CoffeeScript.eval = (code, options = {}) ->
- options.bare ?= on
- eval CoffeeScript.compile code, options |
Running code does not provide access to this scope. | CoffeeScript.run = (code, options = {}) ->
- options.bare = on
- Function(CoffeeScript.compile code, options)() |
If we're not in a browser environment, we're finished with the public API. | return unless window? |
Load a remote script from the current domain via XHR. | CoffeeScript.load = (url, callback, options = {}) ->
- xhr = if window.ActiveXObject
- new window.ActiveXObject('Microsoft.XMLHTTP')
- else
- new XMLHttpRequest()
- xhr.open 'GET', url, true
- xhr.overrideMimeType 'text/plain' if 'overrideMimeType' of xhr
- xhr.onreadystatechange = ->
- if xhr.readyState is 4
- if xhr.status in [0, 200]
- CoffeeScript.run xhr.responseText, options
- else
- throw new Error "Could not load #{url}"
- callback() if callback
- xhr.send null |
Activate CoffeeScript in the browser by having it compile and evaluate + + + + +
+
+
+
| runScripts = ->
- scripts = document.getElementsByTagName 'script'
- coffeetypes = ['text/coffeescript', 'text/literate-coffeescript']
- coffees = (s for s in scripts when s.type in coffeetypes)
- index = 0
- length = coffees.length
- do execute = ->
- script = coffees[index++]
- mediatype = script?.type
- if mediatype in coffeetypes
- options = {literate: mediatype is 'text/literate-coffeescript'}
- if script.src
- CoffeeScript.load script.src, execute, options
- else
- CoffeeScript.run script.innerHTML, options
- execute()
- null |
Listen for window load, both in browsers and in IE. | if window.addEventListener
- addEventListener 'DOMContentLoaded', runScripts, no
-else
- attachEvent 'onload', runScripts
-
- |
runScripts = ->
+ scripts = window.document.getElementsByTagName 'script'
+ coffeetypes = ['text/coffeescript', 'text/literate-coffeescript']
+ coffees = (s for s in scripts when s.type in coffeetypes)
+ index = 0
+ length = coffees.length
+ do execute = ->
+ script = coffees[index++]
+ mediatype = script?.type
+ if mediatype in coffeetypes
+ options = {literate: mediatype is 'text/literate-coffeescript'}
+ if script.src
+ CoffeeScript.load script.src, execute, options
+ else
+ options.sourceFiles = ['embedded']
+ CoffeeScript.run script.innerHTML, options
+ execute()
+ null
Listen for window load, both in decent browsers and in IE. +
+ +if window.addEventListener
+ window.addEventListener 'DOMContentLoaded', runScripts, no
+else
+ window.attachEvent 'onload', runScripts
cake.coffee | |
---|---|
+
+
+
| |
External dependencies. | fs = require 'fs'
-path = require 'path'
-helpers = require './helpers'
-optparse = require './optparse'
-CoffeeScript = require './coffee-script'
-
-existsSync = fs.existsSync or path.existsSync |
Keep track of the list of defined tasks, the accepted options, and so on. | tasks = {}
-options = {}
-switches = []
-oparse = null |
Mixin the top-level Cake functions for Cakefiles to use directly. | helpers.extend global, |
Define a Cake task with a short name, an optional sentence description, -and the function to run as the action itself. | task: (name, description, action) ->
- [action, description] = [description, action] unless action
- tasks[name] = {name, description, action} |
Define an option that the Cakefile accepts. The parsed options hash, +current directory's Cakefile. + + + + + + + +
+
+
+
+
+ ¶
+
+ External dependencies. + + +fs = require 'fs'
+path = require 'path'
+helpers = require './helpers'
+optparse = require './optparse'
+CoffeeScript = require './coffee-script'
+
+existsSync = fs.existsSync or path.existsSync
+
+
+
+
+ ¶
+
+ Keep track of the list of defined tasks, the accepted options, and so on. + + +tasks = {}
+options = {}
+switches = []
+oparse = null
+
+
+
+
+ ¶
+
+ Mixin the top-level Cake functions for Cakefiles to use directly. + + +helpers.extend global,
+
+
+
+
+ ¶
+
+ Define a Cake task with a short name, an optional sentence description, +and the function to run as the action itself. + + + task: (name, description, action) ->
+ [action, description] = [description, action] unless action
+ tasks[name] = {name, description, action}
+
+
+ ¶
+
+ Define an option that the Cakefile accepts. The parsed options hash, containing all of the command-line options passed, will be made available -as the first argument to the action. | option: (letter, flag, description) ->
- switches.push [letter, flag, description] |
Invoke another task in the current Cakefile. | invoke: (name) ->
- missingTask name unless tasks[name]
- tasks[name].action options |
Run option: (letter, flag, description) ->
+ switches.push [letter, flag, description]
+
+
+
+
+ ¶
+
+ Invoke another task in the current Cakefile. + + + invoke: (name) ->
+ missingTask name unless tasks[name]
+ tasks[name].action options
+
+
+ ¶
+
+ Run | exports.run = ->
- global.__originalDirname = fs.realpathSync '.'
- process.chdir cakefileDirectory __originalDirname
- args = process.argv[2..]
- CoffeeScript.run fs.readFileSync('Cakefile').toString(), filename: 'Cakefile'
- oparse = new optparse.OptionParser switches
- return printTasks() unless args.length
- try
- options = oparse.parse(args)
- catch e
- return fatalError "#{e}"
- invoke arg for arg in options.arguments |
Display the list of Cake tasks in a format similar to | printTasks = ->
- relative = path.relative or path.resolve
- cakefilePath = path.join relative(__originalDirname, process.cwd()), 'Cakefile'
- console.log "#{cakefilePath} defines the following tasks:\n"
- for name, task of tasks
- spaces = 20 - name.length
- spaces = if spaces > 0 then Array(spaces + 1).join(' ') else ''
- desc = if task.description then "# #{task.description}" else ''
- console.log "cake #{name}#{spaces} #{desc}"
- console.log oparse.help() if switches.length |
Print an error and exit when attempting to use an invalid task/option. | fatalError = (message) ->
- console.error message + '\n'
- console.log 'To see a list of all tasks/options, run "cake"'
- process.exit 1
-
-missingTask = (task) -> fatalError "No such task: #{task}" |
When | cakefileDirectory = (dir) ->
- return dir if existsSync path.join dir, 'Cakefile'
- parent = path.normalize path.join dir, '..'
- return cakefileDirectory parent unless parent is dir
- throw new Error "Cakefile not found in #{process.cwd()}"
-
- |
exports.run = ->
+ global.__originalDirname = fs.realpathSync '.'
+ process.chdir cakefileDirectory __originalDirname
+ args = process.argv[2..]
+ CoffeeScript.run fs.readFileSync('Cakefile').toString(), filename: 'Cakefile'
+ oparse = new optparse.OptionParser switches
+ return printTasks() unless args.length
+ try
+ options = oparse.parse(args)
+ catch e
+ return fatalError "#{e}"
+ invoke arg for arg in options.arguments
Display the list of Cake tasks in a format similar to rake -T
+
printTasks = ->
+ relative = path.relative or path.resolve
+ cakefilePath = path.join relative(__originalDirname, process.cwd()), 'Cakefile'
+ console.log "#{cakefilePath} defines the following tasks:\n"
+ for name, task of tasks
+ spaces = 20 - name.length
+ spaces = if spaces > 0 then Array(spaces + 1).join(' ') else ''
+ desc = if task.description then "# #{task.description}" else ''
+ console.log "cake #{name}#{spaces} #{desc}"
+ console.log oparse.help() if switches.length
Print an error and exit when attempting to use an invalid task/option. +
+ +fatalError = (message) ->
+ console.error message + '\n'
+ console.log 'To see a list of all tasks/options, run "cake"'
+ process.exit 1
+
+missingTask = (task) -> fatalError "No such task: #{task}"
When cake
is invoked, search in the current and all parent directories
+to find the relevant Cakefile.
+
cakefileDirectory = (dir) ->
+ return dir if existsSync path.join dir, 'Cakefile'
+ parent = path.normalize path.join dir, '..'
+ return cakefileDirectory parent unless parent is dir
+ throw new Error "Cakefile not found in #{process.cwd()}"
coffee-script.coffee | |
---|---|
CoffeeScript can be used both on the server, as a command-line compiler based -on Node.js/V8, or to run CoffeeScripts directly in the browser. This module + + + + +
+
+
+
| fs = require 'fs'
-path = require 'path'
-{Lexer} = require './lexer'
-{parser} = require './parser'
-helpers = require './helpers'
-vm = require 'vm'
-sourcemap = require './sourcemap' |
Load and run a CoffeeScript file for Node, stripping any | loadFile = (module, filename) ->
- raw = fs.readFileSync filename, 'utf8'
- stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw
- module._compile compile(stripped, {filename, literate: helpers.isLiterate filename}), filename
-
-if require.extensions
- for ext in ['.coffee', '.litcoffee', '.md', '.coffee.md']
- require.extensions[ext] = loadFile |
The current CoffeeScript version number. | exports.VERSION = '1.6.1' |
Expose helpers for testing. | exports.helpers = helpers |
Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler. - -If
+fs = require 'fs'
+vm = require 'vm'
+path = require 'path'
+child_process = require 'child_process'
+{Lexer} = require './lexer'
+{parser} = require './parser'
+helpers = require './helpers'
+SourceMap = require './sourcemap'
+
+
+
+
+ ¶
+
+ The current CoffeeScript version number. + + +exports.VERSION = '1.6.3'
+
+
+
+
+ ¶
+
+ Expose helpers for testing. + + +exports.helpers = helpers
+
+
+ ¶
+
+ Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler. + +If This returns a javascript string, unless | exports.compile = compile = (code, options = {}) ->
- {merge} = exports.helpers
- try
-
- if options.sourceMap
- coffeeFile = helpers.baseFileName options.filename
- jsFile = helpers.baseFileName(options.filename, yes) + ".js"
- sourceMap = new sourcemap.SourceMap()
-
- fragments = (parser.parse lexer.tokenize(code, options)).compileToFragments options
-
- currentLine = 0
- currentLine += 1 if options.header
- currentLine += 1 if options.sourceMap
- currentColumn = 0
- js = ""
- for fragment in fragments |
Update the sourcemap with data from each fragment | if sourceMap
- if fragment.locationData
- sourceMap.addMapping(
- [fragment.locationData.first_line, fragment.locationData.first_column],
- [currentLine, currentColumn],
- {noReplace: true})
- newLines = helpers.count fragment.code, "\n"
- currentLine += newLines
- currentColumn = fragment.code.length - (if newLines then fragment.code.lastIndexOf "\n" else 0) |
Copy the code from each fragment into the final JavaScript. | js += fragment.code
-
- catch err
- err.message = "In #{options.filename}, #{err.message}" if options.filename
- throw err
-
- if options.header
- header = "Generated by CoffeeScript #{@VERSION}"
- js = "// #{header}\n#{js}"
-
- if options.sourceMap
- answer = {js}
- if sourceMap
- answer.sourceMap = sourceMap
- answer.v3SourceMap = sourcemap.generateV3SourceMap sourceMap, coffeeFile, jsFile
- answer
- else
- js |
Tokenize a string of CoffeeScript code, and return the array of tokens. | exports.tokens = (code, options) ->
- lexer.tokenize code, options |
Parse a string of CoffeeScript code or an array of lexed tokens, and +lookups. + + + + +exports.compile = compile = (code, options = {}) ->
+ {merge} = helpers
+
+ if options.sourceMap
+ map = new SourceMap
+
+ fragments = parser.parse(lexer.tokenize code, options).compileToFragments options
+
+ currentLine = 0
+ currentLine += 1 if options.header
+ currentLine += 1 if options.shiftLine
+ currentColumn = 0
+ js = ""
+ for fragment in fragments
+
+
+
+
+ ¶
+
+ Update the sourcemap with data from each fragment + + + if options.sourceMap
+ if fragment.locationData
+ map.add(
+ [fragment.locationData.first_line, fragment.locationData.first_column]
+ [currentLine, currentColumn]
+ {noReplace: true})
+ newLines = helpers.count fragment.code, "\n"
+ currentLine += newLines
+ currentColumn = fragment.code.length - (if newLines then fragment.code.lastIndexOf "\n" else 0)
+
+
+
+
+ ¶
+
+ Copy the code from each fragment into the final JavaScript. + + + js += fragment.code
+
+ if options.header
+ header = "Generated by CoffeeScript #{@VERSION}"
+ js = "// #{header}\n#{js}"
+
+ if options.sourceMap
+ answer = {js}
+ answer.sourceMap = map
+ answer.v3SourceMap = map.generate(options, code)
+ answer
+ else
+ js
+
+
+
+
+ ¶
+
+ Tokenize a string of CoffeeScript code, and return the array of tokens. + + +exports.tokens = (code, options) ->
+ lexer.tokenize code, options
+
+
+ ¶
+
+ Parse a string of CoffeeScript code or an array of lexed tokens, and
return the AST. You can then compile it by calling | exports.nodes = (source, options) ->
- if typeof source is 'string'
- parser.parse lexer.tokenize source, options
- else
- parser.parse source |
Compile and execute a string of CoffeeScript (on the server), correctly
-setting | exports.run = (code, options = {}) ->
- mainModule = require.main |
Set the filename. | mainModule.filename = process.argv[1] =
- if options.filename then fs.realpathSync(options.filename) else '.' |
Clear the module cache. | mainModule.moduleCache and= {} |
Assign paths for node_modules loading | mainModule.paths = require('module')._nodeModulePaths path.dirname fs.realpathSync options.filename |
Compile. | if not helpers.isCoffee(mainModule.filename) or require.extensions
- mainModule._compile compile(code, options), mainModule.filename
- else
- mainModule._compile code, mainModule.filename |
Compile and evaluate a string of CoffeeScript (in a Node.js-like environment). -The CoffeeScript REPL uses this to run the input. | exports.eval = (code, options = {}) ->
- return unless code = code.trim()
- Script = vm.Script
- if Script
- if options.sandbox?
- if options.sandbox instanceof Script.createContext().constructor
- sandbox = options.sandbox
- else
- sandbox = Script.createContext()
- sandbox[k] = v for own k, v of options.sandbox
- sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox
- else
- sandbox = global
- sandbox.__filename = options.filename || 'eval'
- sandbox.__dirname = path.dirname sandbox.__filename |
define module/require only if they chose not to specify their own | unless sandbox isnt global or sandbox.module or sandbox.require
- Module = require 'module'
- sandbox.module = _module = new Module(options.modulename || 'eval')
- sandbox.require = _require = (path) -> Module._load path, _module, true
- _module.filename = sandbox.__filename
- _require[r] = require[r] for r in Object.getOwnPropertyNames require when r isnt 'paths' |
use the same hack node currently uses for their own REPL | _require.paths = _module.paths = Module._nodeModulePaths process.cwd()
- _require.resolve = (request) -> Module._resolveFilename request, _module
- o = {}
- o[k] = v for own k, v of options
- o.bare = on # ensure return value
- js = compile code, o
- if sandbox is global
- vm.runInThisContext js
- else
- vm.runInContext js, sandbox |
Instantiate a Lexer for our use here. | lexer = new Lexer |
The real Lexer produces a generic stream of tokens. This object provides a
+or traverse it by using exports.nodes = (source, options) ->
+ if typeof source is 'string'
+ parser.parse lexer.tokenize source, options
+ else
+ parser.parse source
+
+
+
+
+ ¶
+
+ Compile and execute a string of CoffeeScript (on the server), correctly
+setting exports.run = (code, options = {}) ->
+ mainModule = require.main
+ options.sourceMap ?= true
+
+
+
+
+ ¶
+
+ Set the filename. + + + mainModule.filename = process.argv[1] =
+ if options.filename then fs.realpathSync(options.filename) else '.'
+
+
+
+
+ ¶
+
+ Clear the module cache. + + + mainModule.moduleCache and= {}
+
+
+
+
+ ¶
+
+ Assign paths for node_modules loading + + + mainModule.paths = require('module')._nodeModulePaths path.dirname fs.realpathSync options.filename or '.'
+
+
+
+
+ ¶
+
+ Compile. + + + if not helpers.isCoffee(mainModule.filename) or require.extensions
+ answer = compile(code, options)
+
+
+
+
+ ¶
+
+ Attach sourceMap object to sourceMaps[options.filename] so that +it is accessible by Error.prepareStackTrace. + + + do patchStackTrace
+ sourceMaps[mainModule.filename] = answer.sourceMap
+ mainModule._compile answer.js, mainModule.filename
+ else
+ mainModule._compile code, mainModule.filename
+
+
+
+
+ ¶
+
+ Compile and evaluate a string of CoffeeScript (in a Node.js-like environment). +The CoffeeScript REPL uses this to run the input. + + +exports.eval = (code, options = {}) ->
+ return unless code = code.trim()
+ Script = vm.Script
+ if Script
+ if options.sandbox?
+ if options.sandbox instanceof Script.createContext().constructor
+ sandbox = options.sandbox
+ else
+ sandbox = Script.createContext()
+ sandbox[k] = v for own k, v of options.sandbox
+ sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox
+ else
+ sandbox = global
+ sandbox.__filename = options.filename || 'eval'
+ sandbox.__dirname = path.dirname sandbox.__filename
+
+
+
+
+ ¶
+
+ define module/require only if they chose not to specify their own + + + unless sandbox isnt global or sandbox.module or sandbox.require
+ Module = require 'module'
+ sandbox.module = _module = new Module(options.modulename || 'eval')
+ sandbox.require = _require = (path) -> Module._load path, _module, true
+ _module.filename = sandbox.__filename
+ _require[r] = require[r] for r in Object.getOwnPropertyNames require when r isnt 'paths'
+
+
+
+
+ ¶
+
+ use the same hack node currently uses for their own REPL + + + _require.paths = _module.paths = Module._nodeModulePaths process.cwd()
+ _require.resolve = (request) -> Module._resolveFilename request, _module
+ o = {}
+ o[k] = v for own k, v of options
+ o.bare = on # ensure return value
+ js = compile code, o
+ if sandbox is global
+ vm.runInThisContext js
+ else
+ vm.runInContext js, sandbox
+
+
+
+
+ ¶
+
+ Load and run a CoffeeScript file for Node, stripping any loadFile = (module, filename) ->
+ raw = fs.readFileSync filename, 'utf8'
+ stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw
+ answer = compile(stripped, {filename, sourceMap: true, literate: helpers.isLiterate filename})
+ sourceMaps[filename] = answer.sourceMap
+ module._compile answer.js, filename
+
+
+
+
+ ¶
+
+ If the installed version of Node supports if require.extensions
+ for ext in ['.coffee', '.litcoffee', '.coffee.md']
+ require.extensions[ext] = loadFile
+
+
+
+
+ ¶
+
+ Patch Node's module loader to be able to handle mult-dot extensions. +This is a horrible thing that should not be required. Perhaps, one day, +when a truly benevolent dictator comes to rule over the Republik of Node, +it won't be. + + + Module = require 'module'
+
+ findExtension = (filename) ->
+ extensions = path.basename(filename).split '.'
+
+
+
+
+ ¶
+
+ Remove the initial dot from dotfiles. + + + extensions.shift() if extensions[0] is ''
+
+
+
+
+ ¶
+
+ Start with the longest possible extension and work our way shortwards. + + + while extensions.shift()
+ curExtension = '.' + extensions.join '.'
+ return curExtension if Module._extensions[curExtension]
+ '.js'
+
+ Module::load = (filename) ->
+ @filename = filename
+ @paths = Module._nodeModulePaths path.dirname filename
+ extension = findExtension filename
+ Module._extensions[extension](this, filename)
+ @loaded = true
+
+
+
+
+ ¶
+
+ If we're on Node, patch if child_process
+ {fork} = child_process
+ child_process.fork = (path, args = [], options = {}) ->
+ execPath = if helpers.isCoffee(path) then 'coffee' else null
+ if not Array.isArray args
+ args = []
+ options = args or {}
+ options.execPath or= execPath
+ fork path, args, options
+
+
+
+
+ ¶
+
+ Instantiate a Lexer for our use here. + + +lexer = new Lexer
+
+
+ ¶
+
+ The real Lexer produces a generic stream of tokens. This object provides a thin wrapper around it, compatible with the Jison API. We can then pass it -directly as a "Jison lexer". | parser.lexer =
- lex: ->
- token = @tokens[@pos++]
- if token
- [tag, @yytext, @yylloc] = token
- @yylineno = @yylloc.first_line
- else
- tag = ''
-
- tag
- setInput: (@tokens) ->
- @pos = 0
- upcomingInput: ->
- ""
-
-parser.yy = require './nodes'
-
- |
parser.lexer =
+ lex: ->
+ token = @tokens[@pos++]
+ if token
+ [tag, @yytext, @yylloc] = token
+ @yylineno = @yylloc.first_line
+ else
+ tag = ''
+
+ tag
+ setInput: (@tokens) ->
+ @pos = 0
+ upcomingInput: ->
+ ""
Make all the AST nodes visible to the parser. +
+ +parser.yy = require './nodes'
Override Jison's default error handling function. +
+ +parser.yy.parseError = (message, {token}) ->
Disregard Jison's message, it contains redundant line numer information. +
+ + message = "unexpected #{if token is 1 then 'end of input' else token}"
The second argument has a loc
property, which should have the location
+data for this token. Unfortunately, Jison seems to send an outdated loc
+(from the previous token), so we take the location information directly
+from the lexer.
+
helpers.throwSyntaxError message, parser.lexer.yylloc
Based on michaelficarra/CoffeeScriptRedux +NodeJS / V8 have no support for transforming positions in stack traces using +sourceMap, so we must monkey-patch Error to display CoffeeScript source +positions. +
+ +
+patched = false
Map of filenames -> sourceMap object. +
+ +sourceMaps = {}
+
+patchStackTrace = ->
+ return if patched
+ patched = true
+ mainModule = require.main
(Assigning to a property of the Module object in the normal module cache is +unsuitable, because node deletes those objects from the cache if an +exception is thrown in the module body.) +
+ +
+ Error.prepareStackTrace = (err, stack) ->
+ sourceFiles = {}
+
+ getSourceMapping = (filename, line, column) ->
+ sourceMap = sourceMaps[filename]
+ answer = sourceMap.sourceLocation [line - 1, column - 1] if sourceMap
+ if answer then [answer[0] + 1, answer[1] + 1] else null
+
+ frames = for frame in stack
+ break if frame.getFunction() is exports.run
+ " at #{formatSourcePosition frame, getSourceMapping}"
+
+ "#{err.name}: #{err.message ? ''}\n#{frames.join '\n'}\n"
Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js +Modified to handle sourceMap +
+ +formatSourcePosition = (frame, getSourceMapping) ->
+ fileName = undefined
+ fileLocation = ''
+
+ if frame.isNative()
+ fileLocation = "native"
+ else
+ if frame.isEval()
+ fileName = frame.getScriptNameOrSourceURL()
+ fileLocation = "#{frame.getEvalOrigin()}, " unless fileName
+ else
+ fileName = frame.getFileName()
+
+ fileName or= "<anonymous>"
+
+ line = frame.getLineNumber()
+ column = frame.getColumnNumber()
Check for a sourceMap position +
+ + source = getSourceMapping fileName, line, column
+ fileLocation =
+ if source
+ "#{fileName}:#{source[0]}:#{source[1]}, <js>:#{line}:#{column}"
+ else
+ "#{fileName}:#{line}:#{column}"
+
+
+ functionName = frame.getFunctionName()
+ isConstructor = frame.isConstructor()
+ isMethodCall = not (frame.isToplevel() or isConstructor)
+
+ if isMethodCall
+ methodName = frame.getMethodName()
+ typeName = frame.getTypeName()
+
+ if functionName
+ tp = as = ''
+ if typeName and functionName.indexOf typeName
+ tp = "#{typeName}."
+ if methodName and functionName.indexOf(".#{methodName}") isnt functionName.length - methodName.length - 1
+ as = " [as #{methodName}]"
+
+ "#{tp}#{functionName}#{as} (#{fileLocation})"
+ else
+ "#{typeName}.#{methodName or '<anonymous>'} (#{fileLocation})"
+ else if isConstructor
+ "new #{functionName or '<anonymous>'} (#{fileLocation})"
+ else if functionName
+ "#{functionName} (#{fileLocation})"
+ else
+ fileLocation
command.coffee | |
---|---|
The | |
External dependencies. | fs = require 'fs'
-path = require 'path'
-helpers = require './helpers'
-optparse = require './optparse'
-CoffeeScript = require './coffee-script'
-{spawn, exec} = require 'child_process'
-{EventEmitter} = require 'events'
-
-exists = fs.exists or path.exists |
Allow CoffeeScript to emit Node.js events. | helpers.extend CoffeeScript, new EventEmitter
-
-printLine = (line) -> process.stdout.write line + '\n'
-printWarn = (line) -> process.stderr.write line + '\n'
-
-hidden = (file) -> /^\.|~$/.test file |
The help banner that is printed when | BANNER = '''
- Usage: coffee [options] path/to/script.coffee -- [args]
-
- If called without options, `coffee` will run your script.
-''' |
The list of all the valid option flags that | SWITCHES = [
- ['-b', '--bare', 'compile without a top-level function wrapper']
- ['-c', '--compile', 'compile to JavaScript and save as .js files']
- ['-e', '--eval', 'pass a string from the command line as input']
- ['-h', '--help', 'display this help message']
- ['-i', '--interactive', 'run an interactive CoffeeScript REPL']
- ['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling']
- ['-l', '--lint', 'pipe the compiled JavaScript through JavaScript Lint']
- ['-m', '--map', 'generate source map and save as .map files']
- ['-n', '--nodes', 'print out the parse tree that the parser produces']
- [ '--nodejs [ARGS]', 'pass options directly to the "node" binary']
- ['-o', '--output [DIR]', 'set the output directory for compiled JavaScript']
- ['-p', '--print', 'print out the compiled JavaScript']
- ['-s', '--stdio', 'listen for and compile scripts over stdio']
- ['-t', '--tokens', 'print out the tokens that the lexer/rewriter produce']
- ['-v', '--version', 'display the version number']
- ['-w', '--watch', 'watch scripts for changes and rerun commands']
-] |
Top-level objects shared by all the functions. | opts = {}
-sources = []
-sourceCode = []
-notSources = {}
-watchers = {}
-optionParser = null |
Run
+
+
+
| exports.run = ->
- parseOptions()
- return forkNode() if opts.nodejs
- return usage() if opts.help
- return version() if opts.version
- return require('./repl').start() if opts.interactive
- if opts.watch and !fs.watch
- return printWarn "The --watch feature depends on Node v0.6.0+. You are running #{process.version}."
- return compileStdio() if opts.stdio
- return compileScript null, sources[0] if opts.eval
- return require('./repl').start() unless sources.length
- literals = if opts.run then sources.splice 1 else []
- process.argv = process.argv[0..1].concat literals
- process.argv[0] = 'coffee'
- for source in sources
- compilePath source, yes, path.normalize source |
Compile a path, which could be a script or a directory. If a directory -is passed, recursively compile all '.coffee', '.litcoffee', and '.coffee.md' -extension source files in it and all subdirectories. | compilePath = (source, topLevel, base) ->
- fs.stat source, (err, stats) ->
- throw err if err and err.code isnt 'ENOENT'
- if err?.code is 'ENOENT'
- console.error "File not found: #{source}"
- process.exit 1
- if stats.isDirectory() and path.dirname(source) isnt 'node_modules'
- watchDir source, base if opts.watch
- fs.readdir source, (err, files) ->
- throw err if err and err.code isnt 'ENOENT'
- return if err?.code is 'ENOENT'
- index = sources.indexOf source
- files = files.filter (file) -> not hidden file
- sources[index..index] = (path.join source, file for file in files)
- sourceCode[index..index] = files.map -> null
- files.forEach (file) ->
- compilePath (path.join source, file), no, base
- else if topLevel or helpers.isCoffee source
- watch source, base if opts.watch
- fs.readFile source, (err, code) ->
- throw err if err and err.code isnt 'ENOENT'
- return if err?.code is 'ENOENT'
- compileScript(source, code.toString(), base)
- else
- notSources[source] = yes
- removeSource source, base |
Compile a single source script, containing the given code, according to the
+ exports.run = ->
+ parseOptions()
+ return forkNode() if opts.nodejs
+ return usage() if opts.help
+ return version() if opts.version
+ return require('./repl').start() if opts.interactive
+ if opts.watch and not fs.watch
+ return printWarn "The --watch feature depends on Node v0.6.0+. You are running #{process.version}."
+ return compileStdio() if opts.stdio
+ return compileScript null, sources[0] if opts.eval
+ return require('./repl').start() unless sources.length
+ literals = if opts.run then sources.splice 1 else []
+ process.argv = process.argv[0..1].concat literals
+ process.argv[0] = 'coffee'
+ for source in sources
+ compilePath source, yes, path.normalize source
+
+
+
+
+ ¶
+
+ Compile a path, which could be a script or a directory. If a directory +is passed, recursively compile all '.coffee', '.litcoffee', and '.coffee.md' +extension source files in it and all subdirectories. + + +compilePath = (source, topLevel, base) ->
+ fs.stat source, (err, stats) ->
+ throw err if err and err.code isnt 'ENOENT'
+ if err?.code is 'ENOENT'
+ console.error "File not found: #{source}"
+ process.exit 1
+ if stats.isDirectory() and path.dirname(source) isnt 'node_modules'
+ watchDir source, base if opts.watch
+ fs.readdir source, (err, files) ->
+ throw err if err and err.code isnt 'ENOENT'
+ return if err?.code is 'ENOENT'
+ index = sources.indexOf source
+ files = files.filter (file) -> not hidden file
+ sources[index..index] = (path.join source, file for file in files)
+ sourceCode[index..index] = files.map -> null
+ files.forEach (file) ->
+ compilePath (path.join source, file), no, base
+ else if topLevel or helpers.isCoffee source
+ watch source, base if opts.watch
+ fs.readFile source, (err, code) ->
+ throw err if err and err.code isnt 'ENOENT'
+ return if err?.code is 'ENOENT'
+ compileScript(source, code.toString(), base)
+ else
+ notSources[source] = yes
+ removeSource source, base
+
+
+ ¶
+
+ Compile a single source script, containing the given code, according to the
requested options. If evaluating the script directly sets | compileScript = (file, input, base) ->
- o = opts
- options = compileOptions file
- try
- t = task = {file, input, options}
- CoffeeScript.emit 'compile', task
- if o.tokens then printTokens CoffeeScript.tokens t.input, t.options
- else if o.nodes then printLine CoffeeScript.nodes(t.input, t.options).toString().trim()
- else if o.run then CoffeeScript.run t.input, t.options
- else if o.join and t.file isnt o.join
- sourceCode[sources.indexOf(t.file)] = t.input
- compileJoin()
- else
- compiled = CoffeeScript.compile t.input, t.options
- t.output = compiled
- if o.map
- t.output = compiled.js
- t.sourceMap = compiled.v3SourceMap
-
- CoffeeScript.emit 'success', task
- if o.print
- printLine t.output.trim()
- else if o.compile || o.map
- writeJs base, t.file, t.output, t.sourceMap
- else if o.lint
- lint t.file, t.output
- catch err
- CoffeeScript.emit 'failure', err, task
- return if CoffeeScript.listeners('failure').length
- return printLine err.message + '\x07' if o.watch
- printWarn err instanceof Error and err.stack or "ERROR: #{err}"
- process.exit 1 |
Attach the appropriate listeners to compile scripts incoming over stdin, -and write them back to stdout. | compileStdio = ->
- code = ''
- stdin = process.openStdin()
- stdin.on 'data', (buffer) ->
- code += buffer.toString() if buffer
- stdin.on 'end', ->
- compileScript null, code |
If all of the source files are done being read, concatenate and compile -them together. | joinTimeout = null
-compileJoin = ->
- return unless opts.join
- unless sourceCode.some((code) -> code is null)
- clearTimeout joinTimeout
- joinTimeout = wait 100, ->
- compileScript opts.join, sourceCode.join('\n'), opts.join |
Watch a source CoffeeScript file using compileScript = (file, input, base=null) ->
+ o = opts
+ options = compileOptions file, base
+ try
+ t = task = {file, input, options}
+ CoffeeScript.emit 'compile', task
+ if o.tokens then printTokens CoffeeScript.tokens t.input, t.options
+ else if o.nodes then printLine CoffeeScript.nodes(t.input, t.options).toString().trim()
+ else if o.run then CoffeeScript.run t.input, t.options
+ else if o.join and t.file isnt o.join
+ t.input = helpers.invertLiterate t.input if helpers.isLiterate file
+ sourceCode[sources.indexOf(t.file)] = t.input
+ compileJoin()
+ else
+ compiled = CoffeeScript.compile t.input, t.options
+ t.output = compiled
+ if o.map
+ t.output = compiled.js
+ t.sourceMap = compiled.v3SourceMap
+
+ CoffeeScript.emit 'success', task
+ if o.print
+ printLine t.output.trim()
+ else if o.compile or o.map
+ writeJs base, t.file, t.output, options.jsPath, t.sourceMap
+ catch err
+ CoffeeScript.emit 'failure', err, task
+ return if CoffeeScript.listeners('failure').length
+ useColors = process.stdout.isTTY and not process.env.NODE_DISABLE_COLORS
+ message = helpers.prettyErrorMessage err, file or '[stdin]', input, useColors
+ if o.watch
+ printLine message + '\x07'
+ else
+ printWarn message
+ process.exit 1
+
+
+
+
+ ¶
+
+ Attach the appropriate listeners to compile scripts incoming over stdin, +and write them back to stdout. + + +compileStdio = ->
+ code = ''
+ stdin = process.openStdin()
+ stdin.on 'data', (buffer) ->
+ code += buffer.toString() if buffer
+ stdin.on 'end', ->
+ compileScript null, code
+
+
+
+
+ ¶
+
+ If all of the source files are done being read, concatenate and compile +them together. + + +joinTimeout = null
+compileJoin = ->
+ return unless opts.join
+ unless sourceCode.some((code) -> code is null)
+ clearTimeout joinTimeout
+ joinTimeout = wait 100, ->
+ compileScript opts.join, sourceCode.join('\n'), opts.join
+
+
+ ¶
+
+ Watch a source CoffeeScript file using | watch = (source, base) ->
-
- prevStats = null
- compileTimeout = null
-
- watchErr = (e) ->
- if e.code is 'ENOENT'
- return if sources.indexOf(source) is -1
- try
- rewatch()
- compile()
- catch e
- removeSource source, base, yes
- compileJoin()
- else throw e
-
- compile = ->
- clearTimeout compileTimeout
- compileTimeout = wait 25, ->
- fs.stat source, (err, stats) ->
- return watchErr err if err
- return rewatch() if prevStats and stats.size is prevStats.size and
- stats.mtime.getTime() is prevStats.mtime.getTime()
- prevStats = stats
- fs.readFile source, (err, code) ->
- return watchErr err if err
- compileScript(source, code.toString(), base)
- rewatch()
-
- try
- watcher = fs.watch source, compile
- catch e
- watchErr e
-
- rewatch = ->
- watcher?.close()
- watcher = fs.watch source, compile |
Watch a directory of files for new additions. | watchDir = (source, base) ->
- readdirTimeout = null
- try
- watcher = fs.watch source, ->
- clearTimeout readdirTimeout
- readdirTimeout = wait 25, ->
- fs.readdir source, (err, files) ->
- if err
- throw err unless err.code is 'ENOENT'
- watcher.close()
- return unwatchDir source, base
- for file in files when not hidden(file) and not notSources[file]
- file = path.join source, file
- continue if sources.some (s) -> s.indexOf(file) >= 0
- sources.push file
- sourceCode.push null
- compilePath file, no, base
- catch e
- throw e unless e.code is 'ENOENT'
-
-unwatchDir = (source, base) ->
- prevSources = sources[..]
- toRemove = (file for file in sources when file.indexOf(source) >= 0)
- removeSource file, base, yes for file in toRemove
- return unless sources.some (s, i) -> prevSources[i] isnt s
- compileJoin() |
Remove a file from our source list, and source code cache. Optionally remove -the compiled JS version as well. | removeSource = (source, base, removeJs) ->
- index = sources.indexOf source
- sources.splice index, 1
- sourceCode.splice index, 1
- if removeJs and not opts.join
- jsPath = outputPath source, base
- exists jsPath, (itExists) ->
- if itExists
- fs.unlink jsPath, (err) ->
- throw err if err and err.code isnt 'ENOENT'
- timeLog "removed #{source}" |
Get the corresponding output JavaScript path for a source file. | outputPath = (source, base, extension=".js") ->
- basename = helpers.baseFileName source, yes
- srcDir = path.dirname source
- baseDir = if base is '.' then srcDir else srcDir.substring base.length
- dir = if opts.output then path.join opts.output, baseDir else srcDir
- path.join dir, basename + extension |
Write out a JavaScript source file with the compiled code. By default, files
+such as watch = (source, base) ->
+
+ prevStats = null
+ compileTimeout = null
+
+ watchErr = (e) ->
+ if e.code is 'ENOENT'
+ return if sources.indexOf(source) is -1
+ try
+ rewatch()
+ compile()
+ catch e
+ removeSource source, base, yes
+ compileJoin()
+ else throw e
+
+ compile = ->
+ clearTimeout compileTimeout
+ compileTimeout = wait 25, ->
+ fs.stat source, (err, stats) ->
+ return watchErr err if err
+ return rewatch() if prevStats and stats.size is prevStats.size and
+ stats.mtime.getTime() is prevStats.mtime.getTime()
+ prevStats = stats
+ fs.readFile source, (err, code) ->
+ return watchErr err if err
+ compileScript(source, code.toString(), base)
+ rewatch()
+
+ try
+ watcher = fs.watch source, compile
+ catch e
+ watchErr e
+
+ rewatch = ->
+ watcher?.close()
+ watcher = fs.watch source, compile
+
+
+
+
+ ¶
+
+ Watch a directory of files for new additions. + + +watchDir = (source, base) ->
+ readdirTimeout = null
+ try
+ watcher = fs.watch source, ->
+ clearTimeout readdirTimeout
+ readdirTimeout = wait 25, ->
+ fs.readdir source, (err, files) ->
+ if err
+ throw err unless err.code is 'ENOENT'
+ watcher.close()
+ return unwatchDir source, base
+ for file in files when not hidden(file) and not notSources[file]
+ file = path.join source, file
+ continue if sources.some (s) -> s.indexOf(file) >= 0
+ sources.push file
+ sourceCode.push null
+ compilePath file, no, base
+ catch e
+ throw e unless e.code is 'ENOENT'
+
+unwatchDir = (source, base) ->
+ prevSources = sources[..]
+ toRemove = (file for file in sources when file.indexOf(source) >= 0)
+ removeSource file, base, yes for file in toRemove
+ return unless sources.some (s, i) -> prevSources[i] isnt s
+ compileJoin()
+
+
+
+
+ ¶
+
+ Remove a file from our source list, and source code cache. Optionally remove +the compiled JS version as well. + + +removeSource = (source, base, removeJs) ->
+ index = sources.indexOf source
+ sources.splice index, 1
+ sourceCode.splice index, 1
+ if removeJs and not opts.join
+ jsPath = outputPath source, base
+ exists jsPath, (itExists) ->
+ if itExists
+ fs.unlink jsPath, (err) ->
+ throw err if err and err.code isnt 'ENOENT'
+ timeLog "removed #{source}"
+
+
+
+
+ ¶
+
+ Get the corresponding output JavaScript path for a source file. + + +outputPath = (source, base, extension=".js") ->
+ basename = helpers.baseFileName source, yes, useWinPathSep
+ srcDir = path.dirname source
+ baseDir = if base is '.' then srcDir else srcDir.substring base.length
+ dir = if opts.output then path.join opts.output, baseDir else srcDir
+ path.join dir, basename + extension
+
+
+ ¶
+
+ Write out a JavaScript source file with the compiled code. By default, files
are written out in --output .
+
If | writeJs = (base, sourcePath, js, generatedSourceMap = null) ->
- jsPath = outputPath sourcePath, base
- sourceMapPath = outputPath sourcePath, base, ".map"
- jsDir = path.dirname jsPath
- compile = ->
- if opts.compile
- js = ' ' if js.length <= 0
- if generatedSourceMap then js = "//@ sourceMappingURL=#{helpers.baseFileName sourceMapPath}\n#{js}"
- fs.writeFile jsPath, js, (err) ->
- if err
- printLine err.message
- else if opts.compile and opts.watch
- timeLog "compiled #{sourcePath}"
- if generatedSourceMap
- fs.writeFile sourceMapPath, generatedSourceMap, (err) ->
- if err
- printLine "Could not write source map: #{err.message}"
- exists jsDir, (itExists) ->
- if itExists then compile() else exec "mkdir -p #{jsDir}", compile |
Convenience for cleaner setTimeouts. | wait = (milliseconds, func) -> setTimeout func, milliseconds |
When watching scripts, it's useful to log changes with the timestamp. | timeLog = (message) ->
- console.log "#{(new Date).toLocaleTimeString()} - #{message}" |
Pipe compiled JS through JSLint (requires a working | lint = (file, js) ->
- printIt = (buffer) -> printLine file + ':\t' + buffer.toString().trim()
- conf = __dirname + '/../../extras/jsl.conf'
- jsl = spawn 'jsl', ['-nologo', '-stdin', '-conf', conf]
- jsl.stdout.on 'data', printIt
- jsl.stderr.on 'data', printIt
- jsl.stdin.write js
- jsl.stdin.end() |
Pretty-print a stream of tokens, sans location data. | printTokens = (tokens) ->
- strings = for token in tokens
- tag = token[0]
- value = token[1].toString().replace(/\n/, '\\n')
- "[#{tag} #{value}]"
- printLine strings.join(' ') |
Use the OptionParser module to extract all options from
- | parseOptions = ->
- optionParser = new optparse.OptionParser SWITCHES, BANNER
- o = opts = optionParser.parse process.argv[2..]
- o.compile or= !!o.output
- o.run = not (o.compile or o.print or o.lint or o.map)
- o.print = !! (o.print or (o.eval or o.stdio and o.compile))
- sources = o.arguments
- sourceCode[i] = null for source, i in sources
- return |
The compile-time options to pass to the CoffeeScript compiler. | compileOptions = (filename) ->
- {
- filename
- literate: helpers.isLiterate(filename)
- bare: opts.bare
- header: opts.compile
- sourceMap: opts.map
- } |
Start up a new Node.js instance with the arguments in | forkNode = ->
- nodeArgs = opts.nodejs.split /\s+/
- args = process.argv[1..]
- args.splice args.indexOf('--nodejs'), 2
- spawn process.execPath, nodeArgs.concat(args),
- cwd: process.cwd()
- env: process.env
- customFds: [0, 1, 2] |
Print the | usage = ->
- printLine (new optparse.OptionParser SWITCHES, BANNER).help() |
Print the | version = ->
- printLine "CoffeeScript version #{CoffeeScript.VERSION}"
-
- |
.js
file.
+
+
+
+
+ writeJs = (base, sourcePath, js, jsPath, generatedSourceMap = null) ->
+ sourceMapPath = outputPath sourcePath, base, ".map"
+ jsDir = path.dirname jsPath
+ compile = ->
+ if opts.compile
+ js = ' ' if js.length <= 0
+ if generatedSourceMap then js = "#{js}\n/*\n//@ sourceMappingURL=#{helpers.baseFileName sourceMapPath, no, useWinPathSep}\n*/\n"
+ fs.writeFile jsPath, js, (err) ->
+ if err
+ printLine err.message
+ else if opts.compile and opts.watch
+ timeLog "compiled #{sourcePath}"
+ if generatedSourceMap
+ fs.writeFile sourceMapPath, generatedSourceMap, (err) ->
+ if err
+ printLine "Could not write source map: #{err.message}"
+ exists jsDir, (itExists) ->
+ if itExists then compile() else exec "mkdir -p #{jsDir}", compile
Convenience for cleaner setTimeouts. +
+ +wait = (milliseconds, func) -> setTimeout func, milliseconds
When watching scripts, it's useful to log changes with the timestamp. +
+ +timeLog = (message) ->
+ console.log "#{(new Date).toLocaleTimeString()} - #{message}"
Pretty-print a stream of tokens, sans location data. +
+ +printTokens = (tokens) ->
+ strings = for token in tokens
+ tag = token[0]
+ value = token[1].toString().replace(/\n/, '\\n')
+ "[#{tag} #{value}]"
+ printLine strings.join(' ')
Use the OptionParser module to extract all options from
+process.argv
that are specified in SWITCHES
.
+
parseOptions = ->
+ optionParser = new optparse.OptionParser SWITCHES, BANNER
+ o = opts = optionParser.parse process.argv[2..]
+ o.compile or= !!o.output
+ o.run = not (o.compile or o.print or o.map)
+ o.print = !! (o.print or (o.eval or o.stdio and o.compile))
+ sources = o.arguments
+ sourceCode[i] = null for source, i in sources
+ return
The compile-time options to pass to the CoffeeScript compiler. +
+ +compileOptions = (filename, base) ->
+ answer = {
+ filename
+ literate: opts.literate or helpers.isLiterate(filename)
+ bare: opts.bare
+ header: opts.compile
+ sourceMap: opts.map
+ }
+ if filename
+ if base
+ cwd = process.cwd()
+ jsPath = outputPath filename, base
+ jsDir = path.dirname jsPath
+ answer = helpers.merge answer, {
+ jsPath
+ sourceRoot: path.relative jsDir, cwd
+ sourceFiles: [path.relative cwd, filename]
+ generatedFile: helpers.baseFileName(jsPath, no, useWinPathSep)
+ }
+ else
+ answer = helpers.merge answer,
+ sourceRoot: ""
+ sourceFiles: [helpers.baseFileName filename, no, useWinPathSep]
+ generatedFile: helpers.baseFileName(filename, yes, useWinPathSep) + ".js"
+ answer
Start up a new Node.js instance with the arguments in --nodejs
passed to
+the node
binary, preserving the other options.
+
forkNode = ->
+ nodeArgs = opts.nodejs.split /\s+/
+ args = process.argv[1..]
+ args.splice args.indexOf('--nodejs'), 2
+ spawn process.execPath, nodeArgs.concat(args),
+ cwd: process.cwd()
+ env: process.env
+ customFds: [0, 1, 2]
Print the --help
usage message and exit. Deprecated switches are not
+shown.
+
usage = ->
+ printLine (new optparse.OptionParser SWITCHES, BANNER).help()
Print the --version
message and exit.
+
version = ->
+ printLine "CoffeeScript version #{CoffeeScript.VERSION}"
grammar.coffee | |
---|---|
The CoffeeScript parser is generated by Jison + + + + +
+
+
+
| |
The only dependency is on the Jison.Parser. | {Parser} = require 'jison' |
Jison DSL | |
Since we're going to be wrapped in a function by Jison in any case, if our
+from our rules and saves it into
+
+
+
+
+ ¶
+
+ The only dependency is on the Jison.Parser. + + +{Parser} = require 'jison'
+
+
+
+
+ ¶
+
+ Jison DSL+ +
+
+
+
+
+ ¶
+
+
+
+
+
+ ¶
+
+ Since we're going to be wrapped in a function by Jison in any case, if our action immediately returns a value, we can optimize by removing the function -wrapper and just returning the value directly. | unwrap = /^function\s*\(\)\s*\{\s*return\s*([\s\S]*);\s*\}/ |
Our handy DSL for Jison grammar generation, thanks to +wrapper and just returning the value directly. + + + + +unwrap = /^function\s*\(\)\s*\{\s*return\s*([\s\S]*);\s*\}/
+
+
+ ¶
+
+ Our handy DSL for Jison grammar generation, thanks to Tim Caswell. For every rule in the grammar, we pass the pattern-defining string, the action to run, and extra options, optionally. If no action is specified, we simply pass the value of the -previous nonterminal. | o = (patternString, action, options) ->
- patternString = patternString.replace /\s{2,}/g, ' '
- patternCount = patternString.split(' ').length
- return [patternString, '$$ = $1;', options] unless action
- action = if match = unwrap.exec action then match[1] else "(#{action}())" |
All runtime functions we need are defined on "yy" | action = action.replace /\bnew /g, '$&yy.'
- action = action.replace /\b(?:Block\.wrap|extend)\b/g, 'yy.$&' |
Returns a function which adds location data to the first parameter passed +previous nonterminal. + + + + +o = (patternString, action, options) ->
+ patternString = patternString.replace /\s{2,}/g, ' '
+ patternCount = patternString.split(' ').length
+ return [patternString, '$$ = $1;', options] unless action
+ action = if match = unwrap.exec action then match[1] else "(#{action}())"
+
+
+
+
+ ¶
+
+ All runtime functions we need are defined on "yy" + + + action = action.replace /\bnew /g, '$&yy.'
+ action = action.replace /\b(?:Block\.wrap|extend)\b/g, 'yy.$&'
+
+
+ ¶
+
+ Returns a function which adds location data to the first parameter passed in, and returns the parameter. If the parameter is not a node, it will -just be passed through unaffected. | addLocationDataFn = (first, last) ->
- if not last
- "yy.addLocationDataFn(@#{first})"
- else
- "yy.addLocationDataFn(@#{first}, @#{last})"
+just be passed through unaffected.
+
- action = action.replace /LOC\(([0-9]*)\)/g, addLocationDataFn('$1')
- action = action.replace /LOC\(([0-9]*),\s*([0-9]*)\)/g, addLocationDataFn('$1', '$2')
+ addLocationDataFn = (first, last) ->
+ if not last
+ "yy.addLocationDataFn(@#{first})"
+ else
+ "yy.addLocationDataFn(@#{first}, @#{last})"
- [patternString, "$$ = #{addLocationDataFn(1, patternCount)}(#{action});", options] |
Grammatical Rules | |
In all of the rules that follow, you'll see the name of the nonterminal as -the key to a list of alternative matches. With each match's action, the -dollar-sign variables are provided by Jison as references to the value of -their numeric position, so in this rule: + action = action.replace /LOC\(([0-9]*)\)/g, addLocationDataFn('$1') + action = action.replace /LOC\(([0-9]*),\s*([0-9]*)\)/g, addLocationDataFn('$1', '$2') -
+ [patternString, "$$ = #{addLocationDataFn(1, patternCount)}(#{action});", options]
+
+
+
+
+
+
+
+
+
+ ¶
+
+ Grammatical Rules+
+
+
+
+
+ ¶
+
+
+
+
+
+ ¶
+
+ In all of the rules that follow, you'll see the name of the nonterminal as +the key to a list of alternative matches. With each match's action, the +dollar-sign variables are provided by Jison as references to the value of +their numeric position, so in this rule: + + +
| grammar = |
The Root is the top-level node in the syntax tree. Since we parse bottom-up, -all parsing must end here. | Root: [
- o '', -> new Block
- o 'Body'
- o 'Block TERMINATOR'
- ] |
Any list of statements and expressions, separated by line breaks or semicolons. | Body: [
- o 'Line', -> Block.wrap [$1]
- o 'Body TERMINATOR Line', -> $1.push $3
- o 'Body TERMINATOR'
- ] |
Block and statements, which make up a line in a body. | Line: [
- o 'Expression'
- o 'Statement'
- ] |
Pure statements which cannot be expressions. | Statement: [
- o 'Return'
- o 'Comment'
- o 'STATEMENT', -> new Literal $1
- ] |
All the different types of expressions in our language. The basic unit of
+ grammar =
+
+
+
+
+ ¶
+
+ The Root is the top-level node in the syntax tree. Since we parse bottom-up, +all parsing must end here. + + + Root: [
+ o '', -> new Block
+ o 'Body'
+ o 'Block TERMINATOR'
+ ]
+
+
+
+
+ ¶
+
+ Any list of statements and expressions, separated by line breaks or semicolons. + + + Body: [
+ o 'Line', -> Block.wrap [$1]
+ o 'Body TERMINATOR Line', -> $1.push $3
+ o 'Body TERMINATOR'
+ ]
+
+
+
+
+ ¶
+
+ Block and statements, which make up a line in a body. + + + Line: [
+ o 'Expression'
+ o 'Statement'
+ ]
+
+
+
+
+ ¶
+
+ Pure statements which cannot be expressions. + + + Statement: [
+ o 'Return'
+ o 'Comment'
+ o 'STATEMENT', -> new Literal $1
+ ]
+
+
+ ¶
+
+ All the different types of expressions in our language. The basic unit of CoffeeScript is the Expression -- everything that can be an expression is one. Blocks serve as the building blocks of many other rules, making -them somewhat circular. | Expression: [
- o 'Value'
- o 'Invocation'
- o 'Code'
- o 'Operation'
- o 'Assign'
- o 'If'
- o 'Try'
- o 'While'
- o 'For'
- o 'Switch'
- o 'Class'
- o 'Throw'
- ] |
An indented block of expressions. Note that the Rewriter +them somewhat circular. + + + + + Expression: [
+ o 'Value'
+ o 'Invocation'
+ o 'Code'
+ o 'Operation'
+ o 'Assign'
+ o 'If'
+ o 'Try'
+ o 'While'
+ o 'For'
+ o 'Switch'
+ o 'Class'
+ o 'Throw'
+ ] | Block: [
- o 'INDENT OUTDENT', -> new Block
- o 'INDENT Body OUTDENT', -> $2
- ] |
A literal identifier, a variable name or property. | Identifier: [
- o 'IDENTIFIER', -> new Literal $1
- ] |
Alphanumerics are separated from the other Literal matchers because -they can also serve as keys in object literals. | AlphaNumeric: [
- o 'NUMBER', -> new Literal $1
- o 'STRING', -> new Literal $1
- ] |
All of our immediate values. Generally these can be passed straight -through and printed to JavaScript. | Literal: [
- o 'AlphaNumeric'
- o 'JS', -> new Literal $1
- o 'REGEX', -> new Literal $1
- o 'DEBUGGER', -> new Literal $1
- o 'UNDEFINED', -> new Undefined
- o 'NULL', -> new Null
- o 'BOOL', -> new Bool $1
- ] |
Assignment of a variable, property, or index to a value. | Assign: [
- o 'Assignable = Expression', -> new Assign $1, $3
- o 'Assignable = TERMINATOR Expression', -> new Assign $1, $4
- o 'Assignable = INDENT Expression OUTDENT', -> new Assign $1, $4
- ] |
Assignment when it happens within an object literal. The difference from -the ordinary Assign is that these allow numbers and strings as keys. | AssignObj: [
- o 'ObjAssignable', -> new Value $1
- o 'ObjAssignable : Expression', -> new Assign LOC(1)(new Value($1)), $3, 'object'
- o 'ObjAssignable :
- INDENT Expression OUTDENT', -> new Assign LOC(1)(new Value($1)), $4, 'object'
- o 'Comment'
- ]
-
- ObjAssignable: [
- o 'Identifier'
- o 'AlphaNumeric'
- o 'ThisProperty'
- ] |
A return statement from a function body. | Return: [
- o 'RETURN Expression', -> new Return $2
- o 'RETURN', -> new Return
- ] |
A block comment. | Comment: [
- o 'HERECOMMENT', -> new Comment $1
- ] |
The Code node is the function literal. It's defined by an indented block +token stream. + + + + + Block: [
+ o 'INDENT OUTDENT', -> new Block
+ o 'INDENT Body OUTDENT', -> $2
+ ]
+
+
+
+
+ ¶
+
+ A literal identifier, a variable name or property. + + + Identifier: [
+ o 'IDENTIFIER', -> new Literal $1
+ ]
+
+
+
+
+ ¶
+
+ Alphanumerics are separated from the other Literal matchers because +they can also serve as keys in object literals. + + + AlphaNumeric: [
+ o 'NUMBER', -> new Literal $1
+ o 'STRING', -> new Literal $1
+ ]
+
+
+
+
+ ¶
+
+ All of our immediate values. Generally these can be passed straight +through and printed to JavaScript. + + + Literal: [
+ o 'AlphaNumeric'
+ o 'JS', -> new Literal $1
+ o 'REGEX', -> new Literal $1
+ o 'DEBUGGER', -> new Literal $1
+ o 'UNDEFINED', -> new Undefined
+ o 'NULL', -> new Null
+ o 'BOOL', -> new Bool $1
+ ]
+
+
+
+
+ ¶
+
+ Assignment of a variable, property, or index to a value. + + + Assign: [
+ o 'Assignable = Expression', -> new Assign $1, $3
+ o 'Assignable = TERMINATOR Expression', -> new Assign $1, $4
+ o 'Assignable = INDENT Expression OUTDENT', -> new Assign $1, $4
+ ]
+
+
+
+
+ ¶
+
+ Assignment when it happens within an object literal. The difference from +the ordinary Assign is that these allow numbers and strings as keys. + + + AssignObj: [
+ o 'ObjAssignable', -> new Value $1
+ o 'ObjAssignable : Expression', -> new Assign LOC(1)(new Value($1)), $3, 'object'
+ o 'ObjAssignable :
+ INDENT Expression OUTDENT', -> new Assign LOC(1)(new Value($1)), $4, 'object'
+ o 'Comment'
+ ]
+
+ ObjAssignable: [
+ o 'Identifier'
+ o 'AlphaNumeric'
+ o 'ThisProperty'
+ ]
+
+
+
+
+ ¶
+
+ A return statement from a function body. + + + Return: [
+ o 'RETURN Expression', -> new Return $2
+ o 'RETURN', -> new Return
+ ]
+
+
+
+
+ ¶
+
+ A block comment. + + + Comment: [
+ o 'HERECOMMENT', -> new Comment $1
+ ]
+
+
+ ¶
+
+ The Code node is the function literal. It's defined by an indented block of Block preceded by a function arrow, with an optional parameter -list. | Code: [
- o 'PARAM_START ParamList PARAM_END FuncGlyph Block', -> new Code $2, $5, $4
- o 'FuncGlyph Block', -> new Code [], $2, $1
- ] |
CoffeeScript has two different symbols for functions. | FuncGlyph: [
- o '->', -> 'func'
- o '=>', -> 'boundfunc'
- ] |
An optional, trailing comma. | OptComma: [
- o ''
- o ','
- ] |
The list of parameters that a function accepts can be of any length. | ParamList: [
- o '', -> []
- o 'Param', -> [$1]
- o 'ParamList , Param', -> $1.concat $3
- o 'ParamList OptComma TERMINATOR Param', -> $1.concat $4
- o 'ParamList OptComma INDENT ParamList OptComma OUTDENT', -> $1.concat $4
- ] |
A single parameter in a function definition can be ordinary, or a splat -that hoovers up the remaining arguments. | Param: [
- o 'ParamVar', -> new Param $1
- o 'ParamVar ...', -> new Param $1, null, on
- o 'ParamVar = Expression', -> new Param $1, $3
- ] |
Function Parameters | ParamVar: [
- o 'Identifier'
- o 'ThisProperty'
- o 'Array'
- o 'Object'
- ] |
A splat that occurs outside of a parameter list. | Splat: [
- o 'Expression ...', -> new Splat $1
- ] |
Variables and properties that can be assigned to. | SimpleAssignable: [
- o 'Identifier', -> new Value $1
- o 'Value Accessor', -> $1.add $2
- o 'Invocation Accessor', -> new Value $1, [].concat $2
- o 'ThisProperty'
- ] |
Everything that can be assigned to. | Assignable: [
- o 'SimpleAssignable'
- o 'Array', -> new Value $1
- o 'Object', -> new Value $1
- ] |
The types of things that can be treated as values -- assigned to, invoked -as functions, indexed into, named as a class, etc. | Value: [
- o 'Assignable'
- o 'Literal', -> new Value $1
- o 'Parenthetical', -> new Value $1
- o 'Range', -> new Value $1
- o 'This'
- ] |
The general group of accessors into an object, by property, by prototype -or by array index or slice. | Accessor: [
- o '. Identifier', -> new Access $2
- o '?. Identifier', -> new Access $2, 'soak'
- o ':: Identifier', -> [LOC(1)(new Access new Literal('prototype')), LOC(2)(new Access $2)]
- o '?:: Identifier', -> [LOC(1)(new Access new Literal('prototype'), 'soak'), LOC(2)(new Access $2)]
- o '::', -> new Access new Literal 'prototype'
- o 'Index'
- ] |
Indexing into an object or array using bracket notation. | Index: [
- o 'INDEX_START IndexValue INDEX_END', -> $2
- o 'INDEX_SOAK Index', -> extend $2, soak : yes
- ]
-
- IndexValue: [
- o 'Expression', -> new Index $1
- o 'Slice', -> new Slice $1
- ] |
In CoffeeScript, an object literal is simply a list of assignments. | Object: [
- o '{ AssignList OptComma }', -> new Obj $2, $1.generated
- ] |
Assignment of properties within an object literal can be separated by -comma, as in JavaScript, or simply by newline. | AssignList: [
- o '', -> []
- o 'AssignObj', -> [$1]
- o 'AssignList , AssignObj', -> $1.concat $3
- o 'AssignList OptComma TERMINATOR AssignObj', -> $1.concat $4
- o 'AssignList OptComma INDENT AssignList OptComma OUTDENT', -> $1.concat $4
- ] |
Class definitions have optional bodies of prototype property assignments, -and optional references to the superclass. | Class: [
- o 'CLASS', -> new Class
- o 'CLASS Block', -> new Class null, null, $2
- o 'CLASS EXTENDS Expression', -> new Class null, $3
- o 'CLASS EXTENDS Expression Block', -> new Class null, $3, $4
- o 'CLASS SimpleAssignable', -> new Class $2
- o 'CLASS SimpleAssignable Block', -> new Class $2, null, $3
- o 'CLASS SimpleAssignable EXTENDS Expression', -> new Class $2, $4
- o 'CLASS SimpleAssignable EXTENDS Expression Block', -> new Class $2, $4, $5
- ] |
Ordinary function invocation, or a chained series of calls. | Invocation: [
- o 'Value OptFuncExist Arguments', -> new Call $1, $3, $2
- o 'Invocation OptFuncExist Arguments', -> new Call $1, $3, $2
- o 'SUPER', -> new Call 'super', [new Splat new Literal 'arguments']
- o 'SUPER Arguments', -> new Call 'super', $2
- ] |
An optional existence check on a function. | OptFuncExist: [
- o '', -> no
- o 'FUNC_EXIST', -> yes
- ] |
The list of arguments to a function call. | Arguments: [
- o 'CALL_START CALL_END', -> []
- o 'CALL_START ArgList OptComma CALL_END', -> $2
- ] |
A reference to the this current object. | This: [
- o 'THIS', -> new Value new Literal 'this'
- o '@', -> new Value new Literal 'this'
- ] |
A reference to a property on this. | ThisProperty: [
- o '@ Identifier', -> new Value LOC(1)(new Literal('this')), [LOC(2)(new Access($2))], 'this'
- ] |
The array literal. | Array: [
- o '[ ]', -> new Arr []
- o '[ ArgList OptComma ]', -> new Arr $2
- ] |
Inclusive and exclusive range dots. | RangeDots: [
- o '..', -> 'inclusive'
- o '...', -> 'exclusive'
- ] |
The CoffeeScript range literal. | Range: [
- o '[ Expression RangeDots Expression ]', -> new Range $2, $4, $3
- ] |
Array slice literals. | Slice: [
- o 'Expression RangeDots Expression', -> new Range $1, $3, $2
- o 'Expression RangeDots', -> new Range $1, null, $2
- o 'RangeDots Expression', -> new Range null, $2, $1
- o 'RangeDots', -> new Range null, null, $1
- ] |
The ArgList is both the list of objects passed into a function call, +list. + + + + + Code: [
+ o 'PARAM_START ParamList PARAM_END FuncGlyph Block', -> new Code $2, $5, $4
+ o 'FuncGlyph Block', -> new Code [], $2, $1
+ ]
+
+
+
+
+ ¶
+
+ CoffeeScript has two different symbols for functions. FuncGlyph: [
+ o '->', -> 'func'
+ o '=>', -> 'boundfunc'
+ ]
+
+
+
+
+ ¶
+
+ An optional, trailing comma. + + + OptComma: [
+ o ''
+ o ','
+ ]
+
+
+
+
+ ¶
+
+ The list of parameters that a function accepts can be of any length. + + + ParamList: [
+ o '', -> []
+ o 'Param', -> [$1]
+ o 'ParamList , Param', -> $1.concat $3
+ o 'ParamList OptComma TERMINATOR Param', -> $1.concat $4
+ o 'ParamList OptComma INDENT ParamList OptComma OUTDENT', -> $1.concat $4
+ ]
+
+
+
+
+ ¶
+
+ A single parameter in a function definition can be ordinary, or a splat +that hoovers up the remaining arguments. + + + Param: [
+ o 'ParamVar', -> new Param $1
+ o 'ParamVar ...', -> new Param $1, null, on
+ o 'ParamVar = Expression', -> new Param $1, $3
+ ]
+
+
+
+
+ ¶
+
+ Function Parameters + + + ParamVar: [
+ o 'Identifier'
+ o 'ThisProperty'
+ o 'Array'
+ o 'Object'
+ ]
+
+
+
+
+ ¶
+
+ A splat that occurs outside of a parameter list. + + + Splat: [
+ o 'Expression ...', -> new Splat $1
+ ]
+
+
+
+
+ ¶
+
+ Variables and properties that can be assigned to. + + + SimpleAssignable: [
+ o 'Identifier', -> new Value $1
+ o 'Value Accessor', -> $1.add $2
+ o 'Invocation Accessor', -> new Value $1, [].concat $2
+ o 'ThisProperty'
+ ]
+
+
+
+
+ ¶
+
+ Everything that can be assigned to. + + + Assignable: [
+ o 'SimpleAssignable'
+ o 'Array', -> new Value $1
+ o 'Object', -> new Value $1
+ ]
+
+
+
+
+ ¶
+
+ The types of things that can be treated as values -- assigned to, invoked +as functions, indexed into, named as a class, etc. + + + Value: [
+ o 'Assignable'
+ o 'Literal', -> new Value $1
+ o 'Parenthetical', -> new Value $1
+ o 'Range', -> new Value $1
+ o 'This'
+ ]
+
+
+
+
+ ¶
+
+ The general group of accessors into an object, by property, by prototype +or by array index or slice. + + + Accessor: [
+ o '. Identifier', -> new Access $2
+ o '?. Identifier', -> new Access $2, 'soak'
+ o ':: Identifier', -> [LOC(1)(new Access new Literal('prototype')), LOC(2)(new Access $2)]
+ o '?:: Identifier', -> [LOC(1)(new Access new Literal('prototype'), 'soak'), LOC(2)(new Access $2)]
+ o '::', -> new Access new Literal 'prototype'
+ o 'Index'
+ ]
+
+
+
+
+ ¶
+
+ Indexing into an object or array using bracket notation. + + + Index: [
+ o 'INDEX_START IndexValue INDEX_END', -> $2
+ o 'INDEX_SOAK Index', -> extend $2, soak : yes
+ ]
+
+ IndexValue: [
+ o 'Expression', -> new Index $1
+ o 'Slice', -> new Slice $1
+ ]
+
+
+
+
+ ¶
+
+ In CoffeeScript, an object literal is simply a list of assignments. + + + Object: [
+ o '{ AssignList OptComma }', -> new Obj $2, $1.generated
+ ]
+
+
+
+
+ ¶
+
+ Assignment of properties within an object literal can be separated by +comma, as in JavaScript, or simply by newline. + + + AssignList: [
+ o '', -> []
+ o 'AssignObj', -> [$1]
+ o 'AssignList , AssignObj', -> $1.concat $3
+ o 'AssignList OptComma TERMINATOR AssignObj', -> $1.concat $4
+ o 'AssignList OptComma INDENT AssignList OptComma OUTDENT', -> $1.concat $4
+ ]
+
+
+
+
+ ¶
+
+ Class definitions have optional bodies of prototype property assignments, +and optional references to the superclass. + + + Class: [
+ o 'CLASS', -> new Class
+ o 'CLASS Block', -> new Class null, null, $2
+ o 'CLASS EXTENDS Expression', -> new Class null, $3
+ o 'CLASS EXTENDS Expression Block', -> new Class null, $3, $4
+ o 'CLASS SimpleAssignable', -> new Class $2
+ o 'CLASS SimpleAssignable Block', -> new Class $2, null, $3
+ o 'CLASS SimpleAssignable EXTENDS Expression', -> new Class $2, $4
+ o 'CLASS SimpleAssignable EXTENDS Expression Block', -> new Class $2, $4, $5
+ ]
+
+
+
+
+ ¶
+
+ Ordinary function invocation, or a chained series of calls. + + + Invocation: [
+ o 'Value OptFuncExist Arguments', -> new Call $1, $3, $2
+ o 'Invocation OptFuncExist Arguments', -> new Call $1, $3, $2
+ o 'SUPER', -> new Call 'super', [new Splat new Literal 'arguments']
+ o 'SUPER Arguments', -> new Call 'super', $2
+ ]
+
+
+
+
+ ¶
+
+ An optional existence check on a function. + + + OptFuncExist: [
+ o '', -> no
+ o 'FUNC_EXIST', -> yes
+ ]
+
+
+
+
+ ¶
+
+ The list of arguments to a function call. + + + Arguments: [
+ o 'CALL_START CALL_END', -> []
+ o 'CALL_START ArgList OptComma CALL_END', -> $2
+ ]
+
+
+
+
+ ¶
+
+ A reference to the this current object. + + + This: [
+ o 'THIS', -> new Value new Literal 'this'
+ o '@', -> new Value new Literal 'this'
+ ]
+
+
+
+
+ ¶
+
+ A reference to a property on this. + + + ThisProperty: [
+ o '@ Identifier', -> new Value LOC(1)(new Literal('this')), [LOC(2)(new Access($2))], 'this'
+ ]
+
+
+
+
+ ¶
+
+ The array literal. + + + Array: [
+ o '[ ]', -> new Arr []
+ o '[ ArgList OptComma ]', -> new Arr $2
+ ]
+
+
+
+
+ ¶
+
+ Inclusive and exclusive range dots. + + + RangeDots: [
+ o '..', -> 'inclusive'
+ o '...', -> 'exclusive'
+ ]
+
+
+
+
+ ¶
+
+ The CoffeeScript range literal. + + + Range: [
+ o '[ Expression RangeDots Expression ]', -> new Range $2, $4, $3
+ ]
+
+
+
+
+ ¶
+
+ Array slice literals. + + + Slice: [
+ o 'Expression RangeDots Expression', -> new Range $1, $3, $2
+ o 'Expression RangeDots', -> new Range $1, null, $2
+ o 'RangeDots Expression', -> new Range null, $2, $1
+ o 'RangeDots', -> new Range null, null, $1
+ ]
+
+
+ ¶
+
+ The ArgList is both the list of objects passed into a function call, as well as the contents of an array literal -(i.e. comma-separated expressions). Newlines work as well. | ArgList: [
- o 'Arg', -> [$1]
- o 'ArgList , Arg', -> $1.concat $3
- o 'ArgList OptComma TERMINATOR Arg', -> $1.concat $4
- o 'INDENT ArgList OptComma OUTDENT', -> $2
- o 'ArgList OptComma INDENT ArgList OptComma OUTDENT', -> $1.concat $4
- ] |
Valid arguments are Blocks or Splats. | Arg: [
- o 'Expression'
- o 'Splat'
- ] |
Just simple, comma-separated, required arguments (no fancy syntax). We need +(i.e. comma-separated expressions). Newlines work as well. + + + + + ArgList: [
+ o 'Arg', -> [$1]
+ o 'ArgList , Arg', -> $1.concat $3
+ o 'ArgList OptComma TERMINATOR Arg', -> $1.concat $4
+ o 'INDENT ArgList OptComma OUTDENT', -> $2
+ o 'ArgList OptComma INDENT ArgList OptComma OUTDENT', -> $1.concat $4
+ ]
+
+
+
+
+ ¶
+
+ Valid arguments are Blocks or Splats. + + + Arg: [
+ o 'Expression'
+ o 'Splat'
+ ]
+
+
+ ¶
+
+ Just simple, comma-separated, required arguments (no fancy syntax). We need this to be separate from the ArgList for use in Switch blocks, where -having the newlines wouldn't make sense. | SimpleArgs: [
- o 'Expression'
- o 'SimpleArgs , Expression', -> [].concat $1, $3
- ] |
The variants of try/catch/finally exception handling blocks. | Try: [
- o 'TRY Block', -> new Try $2
- o 'TRY Block Catch', -> new Try $2, $3[0], $3[1]
- o 'TRY Block FINALLY Block', -> new Try $2, null, null, $4
- o 'TRY Block Catch FINALLY Block', -> new Try $2, $3[0], $3[1], $5
- ] |
A catch clause names its error and runs a block of code. | Catch: [
- o 'CATCH Identifier Block', -> [$2, $3]
- o 'CATCH Object Block', -> [LOC(2)(new Value($2)), $3]
- ] |
Throw an exception object. | Throw: [
- o 'THROW Expression', -> new Throw $2
- ] |
Parenthetical expressions. Note that the Parenthetical is a Value, +having the newlines wouldn't make sense. + + + + + SimpleArgs: [
+ o 'Expression'
+ o 'SimpleArgs , Expression', -> [].concat $1, $3
+ ]
+
+
+
+
+ ¶
+
+ The variants of try/catch/finally exception handling blocks. + + + Try: [
+ o 'TRY Block', -> new Try $2
+ o 'TRY Block Catch', -> new Try $2, $3[0], $3[1]
+ o 'TRY Block FINALLY Block', -> new Try $2, null, null, $4
+ o 'TRY Block Catch FINALLY Block', -> new Try $2, $3[0], $3[1], $5
+ ]
+
+
+
+
+ ¶
+
+ A catch clause names its error and runs a block of code. + + + Catch: [
+ o 'CATCH Identifier Block', -> [$2, $3]
+ o 'CATCH Object Block', -> [LOC(2)(new Value($2)), $3]
+ o 'CATCH Block', -> [null, $2]
+ ]
+
+
+
+
+ ¶
+
+ Throw an exception object. + + + Throw: [
+ o 'THROW Expression', -> new Throw $2
+ ]
+
+
+ ¶
+
+ Parenthetical expressions. Note that the Parenthetical is a Value, not an Expression, so if you need to use an expression in a place where only values are accepted, wrapping it in parentheses will always do -the trick. | Parenthetical: [
- o '( Body )', -> new Parens $2
- o '( INDENT Body OUTDENT )', -> new Parens $3
- ] |
The condition portion of a while loop. | WhileSource: [
- o 'WHILE Expression', -> new While $2
- o 'WHILE Expression WHEN Expression', -> new While $2, guard: $4
- o 'UNTIL Expression', -> new While $2, invert: true
- o 'UNTIL Expression WHEN Expression', -> new While $2, invert: true, guard: $4
- ] |
The while loop can either be normal, with a block of expressions to execute, -or postfix, with a single expression. There is no do..while. | While: [
- o 'WhileSource Block', -> $1.addBody $2
- o 'Statement WhileSource', -> $2.addBody LOC(1) Block.wrap([$1])
- o 'Expression WhileSource', -> $2.addBody LOC(1) Block.wrap([$1])
- o 'Loop', -> $1
- ]
-
- Loop: [
- o 'LOOP Block', -> new While(LOC(1) new Literal 'true').addBody $2
- o 'LOOP Expression', -> new While(LOC(1) new Literal 'true').addBody LOC(2) Block.wrap [$2]
- ] |
Array, object, and range comprehensions, at the most generic level. +the trick. + + + + + Parenthetical: [
+ o '( Body )', -> new Parens $2
+ o '( INDENT Body OUTDENT )', -> new Parens $3
+ ]
+
+
+
+
+ ¶
+
+ The condition portion of a while loop. + + + WhileSource: [
+ o 'WHILE Expression', -> new While $2
+ o 'WHILE Expression WHEN Expression', -> new While $2, guard: $4
+ o 'UNTIL Expression', -> new While $2, invert: true
+ o 'UNTIL Expression WHEN Expression', -> new While $2, invert: true, guard: $4
+ ]
+
+
+
+
+ ¶
+
+ The while loop can either be normal, with a block of expressions to execute, +or postfix, with a single expression. There is no do..while. + + + While: [
+ o 'WhileSource Block', -> $1.addBody $2
+ o 'Statement WhileSource', -> $2.addBody LOC(1) Block.wrap([$1])
+ o 'Expression WhileSource', -> $2.addBody LOC(1) Block.wrap([$1])
+ o 'Loop', -> $1
+ ]
+
+ Loop: [
+ o 'LOOP Block', -> new While(LOC(1) new Literal 'true').addBody $2
+ o 'LOOP Expression', -> new While(LOC(1) new Literal 'true').addBody LOC(2) Block.wrap [$2]
+ ]
+
+
+ ¶
+
+ Array, object, and range comprehensions, at the most generic level. Comprehensions can either be normal, with a block of expressions to execute, -or postfix, with a single expression. | For: [
- o 'Statement ForBody', -> new For $1, $2
- o 'Expression ForBody', -> new For $1, $2
- o 'ForBody Block', -> new For $2, $1
- ]
-
- ForBody: [
- o 'FOR Range', -> source: LOC(2) new Value($2)
- o 'ForStart ForSource', -> $2.own = $1.own; $2.name = $1[0]; $2.index = $1[1]; $2
- ]
-
- ForStart: [
- o 'FOR ForVariables', -> $2
- o 'FOR OWN ForVariables', -> $3.own = yes; $3
- ] |
An array of all accepted values for a variable inside the loop. -This enables support for pattern matching. | ForValue: [
- o 'Identifier'
- o 'ThisProperty'
- o 'Array', -> new Value $1
- o 'Object', -> new Value $1
- ] |
An array or range comprehension has variables for the current element +or postfix, with a single expression. + + + + + For: [
+ o 'Statement ForBody', -> new For $1, $2
+ o 'Expression ForBody', -> new For $1, $2
+ o 'ForBody Block', -> new For $2, $1
+ ]
+
+ ForBody: [
+ o 'FOR Range', -> source: LOC(2) new Value($2)
+ o 'ForStart ForSource', -> $2.own = $1.own; $2.name = $1[0]; $2.index = $1[1]; $2
+ ]
+
+ ForStart: [
+ o 'FOR ForVariables', -> $2
+ o 'FOR OWN ForVariables', -> $3.own = yes; $3
+ ]
+
+
+
+
+ ¶
+
+ An array of all accepted values for a variable inside the loop. +This enables support for pattern matching. + + + ForValue: [
+ o 'Identifier'
+ o 'ThisProperty'
+ o 'Array', -> new Value $1
+ o 'Object', -> new Value $1
+ ]
+
+
+ ¶
+
+ An array or range comprehension has variables for the current element and (optional) reference to the current index. Or, key, value, in the case -of object comprehensions. | ForVariables: [
- o 'ForValue', -> [$1]
- o 'ForValue , ForValue', -> [$1, $3]
- ] |
The source of a comprehension is an array or object with an optional guard -clause. If it's an array comprehension, you can also choose to step through -in fixed-size increments. | ForSource: [
- o 'FORIN Expression', -> source: $2
- o 'FOROF Expression', -> source: $2, object: yes
- o 'FORIN Expression WHEN Expression', -> source: $2, guard: $4
- o 'FOROF Expression WHEN Expression', -> source: $2, guard: $4, object: yes
- o 'FORIN Expression BY Expression', -> source: $2, step: $4
- o 'FORIN Expression WHEN Expression BY Expression', -> source: $2, guard: $4, step: $6
- o 'FORIN Expression BY Expression WHEN Expression', -> source: $2, step: $4, guard: $6
- ]
-
- Switch: [
- o 'SWITCH Expression INDENT Whens OUTDENT', -> new Switch $2, $4
- o 'SWITCH Expression INDENT Whens ELSE Block OUTDENT', -> new Switch $2, $4, $6
- o 'SWITCH INDENT Whens OUTDENT', -> new Switch null, $3
- o 'SWITCH INDENT Whens ELSE Block OUTDENT', -> new Switch null, $3, $5
- ]
-
- Whens: [
- o 'When'
- o 'Whens When', -> $1.concat $2
- ] |
An individual When clause, with action. | When: [
- o 'LEADING_WHEN SimpleArgs Block', -> [[$2, $3]]
- o 'LEADING_WHEN SimpleArgs Block TERMINATOR', -> [[$2, $3]]
- ] |
The most basic form of if is a condition and an action. The following +of object comprehensions. + + + + + ForVariables: [
+ o 'ForValue', -> [$1]
+ o 'ForValue , ForValue', -> [$1, $3]
+ ]
+
+
+
+
+ ¶
+
+ The source of a comprehension is an array or object with an optional guard +clause. If it's an array comprehension, you can also choose to step through +in fixed-size increments. + + + ForSource: [
+ o 'FORIN Expression', -> source: $2
+ o 'FOROF Expression', -> source: $2, object: yes
+ o 'FORIN Expression WHEN Expression', -> source: $2, guard: $4
+ o 'FOROF Expression WHEN Expression', -> source: $2, guard: $4, object: yes
+ o 'FORIN Expression BY Expression', -> source: $2, step: $4
+ o 'FORIN Expression WHEN Expression BY Expression', -> source: $2, guard: $4, step: $6
+ o 'FORIN Expression BY Expression WHEN Expression', -> source: $2, step: $4, guard: $6
+ ]
+
+ Switch: [
+ o 'SWITCH Expression INDENT Whens OUTDENT', -> new Switch $2, $4
+ o 'SWITCH Expression INDENT Whens ELSE Block OUTDENT', -> new Switch $2, $4, $6
+ o 'SWITCH INDENT Whens OUTDENT', -> new Switch null, $3
+ o 'SWITCH INDENT Whens ELSE Block OUTDENT', -> new Switch null, $3, $5
+ ]
+
+ Whens: [
+ o 'When'
+ o 'Whens When', -> $1.concat $2
+ ]
+
+
+
+
+ ¶
+
+ An individual When clause, with action. + + + When: [
+ o 'LEADING_WHEN SimpleArgs Block', -> [[$2, $3]]
+ o 'LEADING_WHEN SimpleArgs Block TERMINATOR', -> [[$2, $3]]
+ ]
+
+
+ ¶
+
+ The most basic form of if is a condition and an action. The following if-related rules are broken up along these lines in order to avoid -ambiguity. | IfBlock: [
- o 'IF Expression Block', -> new If $2, $3, type: $1
- o 'IfBlock ELSE IF Expression Block', -> $1.addElse new If $4, $5, type: $3
- ] |
The full complement of if expressions, including postfix one-liner -if and unless. | If: [
- o 'IfBlock'
- o 'IfBlock ELSE Block', -> $1.addElse $3
- o 'Statement POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true
- o 'Expression POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true
- ] |
Arithmetic and logical operators, working on one or more operands. +ambiguity. + + + + + IfBlock: [
+ o 'IF Expression Block', -> new If $2, $3, type: $1
+ o 'IfBlock ELSE IF Expression Block', -> $1.addElse new If $4, $5, type: $3
+ ]
+
+
+
+
+ ¶
+
+ The full complement of if expressions, including postfix one-liner +if and unless. + + + If: [
+ o 'IfBlock'
+ o 'IfBlock ELSE Block', -> $1.addElse $3
+ o 'Statement POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true
+ o 'Expression POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true
+ ]
+
+
+ ¶
+
+ Arithmetic and logical operators, working on one or more operands. Here they are grouped by order of precedence. The actual precedence rules are defined at the bottom of the page. It would be shorter if we could combine most of these rules into a single generic Operand OpSymbol Operand -type rule, but in order to make the precedence binding possible, separate -rules are necessary. | Operation: [
- o 'UNARY Expression', -> new Op $1 , $2
- o '- Expression', (-> new Op '-', $2), prec: 'UNARY'
- o '+ Expression', (-> new Op '+', $2), prec: 'UNARY'
-
- o '-- SimpleAssignable', -> new Op '--', $2
- o '++ SimpleAssignable', -> new Op '++', $2
- o 'SimpleAssignable --', -> new Op '--', $1, null, true
- o 'SimpleAssignable ++', -> new Op '++', $1, null, true |
o 'Expression ?', -> new Existence $1
-
- o 'Expression + Expression', -> new Op '+' , $1, $3
- o 'Expression - Expression', -> new Op '-' , $1, $3
-
- o 'Expression MATH Expression', -> new Op $2, $1, $3
- o 'Expression SHIFT Expression', -> new Op $2, $1, $3
- o 'Expression COMPARE Expression', -> new Op $2, $1, $3
- o 'Expression LOGIC Expression', -> new Op $2, $1, $3
- o 'Expression RELATION Expression', ->
- if $2.charAt(0) is '!'
- new Op($2[1..], $1, $3).invert()
- else
- new Op $2, $1, $3
-
- o 'SimpleAssignable COMPOUND_ASSIGN
- Expression', -> new Assign $1, $3, $2
- o 'SimpleAssignable COMPOUND_ASSIGN
- INDENT Expression OUTDENT', -> new Assign $1, $4, $2
- o 'SimpleAssignable COMPOUND_ASSIGN TERMINATOR
- Expression', -> new Assign $1, $4, $2
- o 'SimpleAssignable EXTENDS Expression', -> new Extends $1, $3
- ] | |
Precedence | |
Operators at the top of this list have higher precedence than the ones lower
-down. Following these rules is what makes
-
-And not: - - | operators = [
- ['left', '.', '?.', '::', '?::']
- ['left', 'CALL_START', 'CALL_END']
- ['nonassoc', '++', '--']
- ['left', '?']
- ['right', 'UNARY']
- ['left', 'MATH']
- ['left', '+', '-']
- ['left', 'SHIFT']
- ['left', 'RELATION']
- ['left', 'COMPARE']
- ['left', 'LOGIC']
- ['nonassoc', 'INDENT', 'OUTDENT']
- ['right', '=', ':', 'COMPOUND_ASSIGN', 'RETURN', 'THROW', 'EXTENDS']
- ['right', 'FORIN', 'FOROF', 'BY', 'WHEN']
- ['right', 'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS']
- ['right', 'POST_IF']
-] |
Wrapping Up | |
Finally, now that we have our grammar and our operators, we can create +rules are necessary. + + + + + Operation: [
+ o 'UNARY Expression', -> new Op $1 , $2
+ o '- Expression', (-> new Op '-', $2), prec: 'UNARY'
+ o '+ Expression', (-> new Op '+', $2), prec: 'UNARY'
+
+ o '-- SimpleAssignable', -> new Op '--', $2
+ o '++ SimpleAssignable', -> new Op '++', $2
+ o 'SimpleAssignable --', -> new Op '--', $1, null, true
+ o 'SimpleAssignable ++', -> new Op '++', $1, null, true
+
+
+
+
+ ¶
+
+
+
+ o 'Expression ?', -> new Existence $1
+
+ o 'Expression + Expression', -> new Op '+' , $1, $3
+ o 'Expression - Expression', -> new Op '-' , $1, $3
+
+ o 'Expression MATH Expression', -> new Op $2, $1, $3
+ o 'Expression SHIFT Expression', -> new Op $2, $1, $3
+ o 'Expression COMPARE Expression', -> new Op $2, $1, $3
+ o 'Expression LOGIC Expression', -> new Op $2, $1, $3
+ o 'Expression RELATION Expression', ->
+ if $2.charAt(0) is '!'
+ new Op($2[1..], $1, $3).invert()
+ else
+ new Op $2, $1, $3
+
+ o 'SimpleAssignable COMPOUND_ASSIGN
+ Expression', -> new Assign $1, $3, $2
+ o 'SimpleAssignable COMPOUND_ASSIGN
+ INDENT Expression OUTDENT', -> new Assign $1, $4, $2
+ o 'SimpleAssignable COMPOUND_ASSIGN TERMINATOR
+ Expression', -> new Assign $1, $4, $2
+ o 'SimpleAssignable EXTENDS Expression', -> new Extends $1, $3
+ ]
+
+
+
+
+ ¶
+
+ Precedence+ +
+
+
+
+
+ ¶
+
+
+
+
+
+
+
+ ¶
+
+ Operators at the top of this list have higher precedence than the ones lower
+down. Following these rules is what makes
+And not: + + +
+
+ operators = [
+ ['left', '.', '?.', '::', '?::']
+ ['left', 'CALL_START', 'CALL_END']
+ ['nonassoc', '++', '--']
+ ['left', '?']
+ ['right', 'UNARY']
+ ['left', 'MATH']
+ ['left', '+', '-']
+ ['left', 'SHIFT']
+ ['left', 'RELATION']
+ ['left', 'COMPARE']
+ ['left', 'LOGIC']
+ ['nonassoc', 'INDENT', 'OUTDENT']
+ ['right', '=', ':', 'COMPOUND_ASSIGN', 'RETURN', 'THROW', 'EXTENDS']
+ ['right', 'FORIN', 'FOROF', 'BY', 'WHEN']
+ ['right', 'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS']
+ ['right', 'POST_IF']
+]
+
+
+
+
+ ¶
+
+ Wrapping Up+ +
+
+
+
+
+ ¶
+
+
+
+
+
+ ¶
+
+ Finally, now that we have our grammar and our operators, we can create our Jison.Parser. We do this by processing all of our rules, recording all terminals (every symbol which does not appear as the name of a rule above) -as "tokens". | tokens = []
-for name, alternatives of grammar
- grammar[name] = for alt in alternatives
- for token in alt[0].split ' '
- tokens.push token unless grammar[token]
- alt[1] = "return #{alt[1]}" if name is 'Root'
- alt |
Initialize the Parser with our list of terminal tokens, our grammar +as "tokens". + + + + +tokens = []
+for name, alternatives of grammar
+ grammar[name] = for alt in alternatives
+ for token in alt[0].split ' '
+ tokens.push token unless grammar[token]
+ alt[1] = "return #{alt[1]}" if name is 'Root'
+ alt | exports.parser = new Parser
- tokens : tokens.join ' '
- bnf : grammar
- operators : operators.reverse()
- startSymbol : 'Root'
+(as in Yacc).
+
- |
exports.parser = new Parser
+ tokens : tokens.join ' '
+ bnf : grammar
+ operators : operators.reverse()
+ startSymbol : 'Root'
helpers.coffee | |
---|---|
This file contains the common helper functions that we'd like to share among + + + + +
+
+
+
| |
Peek at the beginning of a given string to see if it matches a sequence. | exports.starts = (string, literal, start) ->
- literal is string.substr start, literal.length |
Peek at the end of a given string to see if it matches a sequence. | exports.ends = (string, literal, back) ->
- len = literal.length
- literal is string.substr string.length - len - (back or 0), len |
Trim out all falsy values from an array. | exports.compact = (array) ->
- item for item in array when item |
Count the number of occurrences of a string in a string. | exports.count = (string, substr) ->
- num = pos = 0
- return 1/0 unless substr.length
- num++ while pos = 1 + string.indexOf substr, pos
- num |
Merge objects, returning a fresh copy with attributes from both sides. +arrays, count characters, that sort of thing. + + + + + + + +
+
+
+
+
+ ¶
+
+ Peek at the beginning of a given string to see if it matches a sequence. + + +exports.starts = (string, literal, start) ->
+ literal is string.substr start, literal.length
+
+
+
+
+ ¶
+
+ Peek at the end of a given string to see if it matches a sequence. + + +exports.ends = (string, literal, back) ->
+ len = literal.length
+ literal is string.substr string.length - len - (back or 0), len
+
+
+
+
+ ¶
+
+ Repeat a string exports.repeat = repeat = (str, n) ->
+
+
+
+
+ ¶
+
+ Use clever algorithm to have O(log(n)) string concatenation operations. + + + res = ''
+ while n > 0
+ res += str if n & 1
+ n >>>= 1
+ str += str
+ res
+
+
+
+
+ ¶
+
+ Trim out all falsy values from an array. + + +exports.compact = (array) ->
+ item for item in array when item
+
+
+
+
+ ¶
+
+ Count the number of occurrences of a string in a string. + + +exports.count = (string, substr) ->
+ num = pos = 0
+ return 1/0 unless substr.length
+ num++ while pos = 1 + string.indexOf substr, pos
+ num
+
+
+ ¶
+
+ Merge objects, returning a fresh copy with attributes from both sides.
Used every time | exports.merge = (options, overrides) ->
- extend (extend {}, options), overrides |
Extend a source object with the properties of another object (shallow copy). | extend = exports.extend = (object, properties) ->
- for key, val of properties
- object[key] = val
- object |
Return a flattened version of an array.
-Handy for getting a list of | exports.flatten = flatten = (array) ->
- flattened = []
- for element in array
- if element instanceof Array
- flattened = flattened.concat flatten element
- else
- flattened.push element
- flattened |
Delete a key from an object, returning the value. Useful when a node is -looking for a particular method in an options hash. | exports.del = (obj, key) ->
- val = obj[key]
- delete obj[key]
- val |
Gets the last item of an array(-like) object. | exports.last = (array, back) -> array[array.length - (back or 0) - 1] |
Typical Array::some | exports.some = Array::some ? (fn) ->
- return true for e in this when fn e
- false |
Merge two jison-style location data objects together.
-If | buildLocationData = (first, last) ->
- if not last
- first
- else
- first_line: first.first_line
- first_column: first.first_column
- last_line: last.last_line
- last_column: last.last_column |
This returns a function which takes an object as a parameter, and if that object is an AST node, -updates that object's locationData. The object is returned either way. | exports.addLocationDataFn = (first, last) ->
- (obj) ->
- if ((typeof obj) is 'object') and (!!obj['updateLocationDataIfMissing'])
- obj.updateLocationDataIfMissing buildLocationData(first, last)
-
- return obj |
Convert jison location data to a string.
- | exports.locationDataToString = (obj) ->
- if ("2" of obj) and ("first_line" of obj[2]) then locationData = obj[2]
- else if "first_line" of obj then locationData = obj
-
- if locationData
- "#{locationData.first_line + 1}:#{locationData.first_column + 1}-" +
- "#{locationData.last_line + 1}:#{locationData.last_column + 1}"
- else
- "No location data" |
A | exports.baseFileName = (file, stripExt = no) ->
- parts = file.split('/')
- file = parts[parts.length - 1]
- return file unless stripExt
- parts = file.split('.')
- parts.pop()
- parts.pop() if parts[parts.length - 1] is 'coffee'
- parts.join('.') |
Determine if a filename represents a CoffeeScript file. | exports.isCoffee = (file) -> /\.((lit)?coffee|coffee\.md)$/.test file |
Determine if a filename represents a Literate CoffeeScript file. | exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file
-
- |
exports.merge = (options, overrides) ->
+ extend (extend {}, options), overrides
Extend a source object with the properties of another object (shallow copy). +
+ +extend = exports.extend = (object, properties) ->
+ for key, val of properties
+ object[key] = val
+ object
Return a flattened version of an array.
+Handy for getting a list of children
from the nodes.
+
exports.flatten = flatten = (array) ->
+ flattened = []
+ for element in array
+ if element instanceof Array
+ flattened = flattened.concat flatten element
+ else
+ flattened.push element
+ flattened
Delete a key from an object, returning the value. Useful when a node is +looking for a particular method in an options hash. +
+ +exports.del = (obj, key) ->
+ val = obj[key]
+ delete obj[key]
+ val
Gets the last item of an array(-like) object. +
+ +exports.last = last = (array, back) -> array[array.length - (back or 0) - 1]
Typical Array::some +
+ +exports.some = Array::some ? (fn) ->
+ return true for e in this when fn e
+ false
Simple function for inverting Literate CoffeeScript code by putting the +documentation in comments, producing a string of CoffeeScript code that +can be compiled "normally". +
+ +exports.invertLiterate = (code) ->
+ maybe_code = true
+ lines = for line in code.split('\n')
+ if maybe_code and /^([ ]{4}|[ ]{0,3}\t)/.test line
+ line
+ else if maybe_code = /^\s*$/.test line
+ line
+ else
+ '# ' + line
+ lines.join '\n'
Merge two jison-style location data objects together.
+If last
is not provided, this will simply return first
.
+
buildLocationData = (first, last) ->
+ if not last
+ first
+ else
+ first_line: first.first_line
+ first_column: first.first_column
+ last_line: last.last_line
+ last_column: last.last_column
This returns a function which takes an object as a parameter, and if that +object is an AST node, updates that object's locationData. +The object is returned either way. +
+ +exports.addLocationDataFn = (first, last) ->
+ (obj) ->
+ if ((typeof obj) is 'object') and (!!obj['updateLocationDataIfMissing'])
+ obj.updateLocationDataIfMissing buildLocationData(first, last)
+
+ return obj
Convert jison location data to a string.
+obj
can be a token, or a locationData.
+
exports.locationDataToString = (obj) ->
+ if ("2" of obj) and ("first_line" of obj[2]) then locationData = obj[2]
+ else if "first_line" of obj then locationData = obj
+
+ if locationData
+ "#{locationData.first_line + 1}:#{locationData.first_column + 1}-" +
+ "#{locationData.last_line + 1}:#{locationData.last_column + 1}"
+ else
+ "No location data"
A .coffee.md
compatible version of basename
, that returns the file sans-extension.
+
exports.baseFileName = (file, stripExt = no, useWinPathSep = no) ->
+ pathSep = if useWinPathSep then /\\|\// else /\//
+ parts = file.split(pathSep)
+ file = parts[parts.length - 1]
+ return file unless stripExt
+ parts = file.split('.')
+ parts.pop()
+ parts.pop() if parts[parts.length - 1] is 'coffee' and parts.length > 1
+ parts.join('.')
Determine if a filename represents a CoffeeScript file. +
+ +exports.isCoffee = (file) -> /\.((lit)?coffee|coffee\.md)$/.test file
Determine if a filename represents a Literate CoffeeScript file. +
+ +exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file
Throws a SyntaxError with a source file location data attached to it in a
+property called location
.
+
exports.throwSyntaxError = (message, location) ->
+ location.last_line ?= location.first_line
+ location.last_column ?= location.first_column
+ error = new SyntaxError message
+ error.location = location
+ throw error
Creates a nice error message like, following the "standard" format +
+exports.prettyErrorMessage = (error, fileName, code, useColors) ->
+ return error.stack or "#{error}" unless error.location
+
+ {first_line, first_column, last_line, last_column} = error.location
+ codeLine = code.split('\n')[first_line]
+ start = first_column
Show only the first line on multi-line errors. +
+ + end = if first_line is last_line then last_column + 1 else codeLine.length
+ marker = repeat(' ', start) + repeat('^', end - start)
+
+ if useColors
+ colorize = (str) -> "\x1B[1;31m#{str}\x1B[0m"
+ codeLine = codeLine[...start] + colorize(codeLine[start...end]) + codeLine[end..]
+ marker = colorize marker
+
+ message = """
+ #{fileName}:#{first_line + 1}:#{first_column + 1}: error: #{error.message}
+ #{codeLine}
+ #{marker}
+ """
Uncomment to add stacktrace. +message += "\n#{error.stack}" +
+ +
+ message
index.coffee | |
---|---|
Loader for CoffeeScript as a Node.js library. | exports[key] = val for key, val of require './coffee-script'
+
- |
Loader for CoffeeScript as a Node.js library. +
+ +exports[key] = val for key, val of require './coffee-script'
lexer.coffee | |
---|---|
The CoffeeScript Lexer. Uses a series of token-matching regexes to attempt + + + + +
+
+
+
| {Rewriter, INVERSES} = require './rewriter' |
Import the helpers we need. | {count, starts, compact, last, locationDataToString} = require './helpers' |
The Lexer Class | |
The Lexer class reads a stream of CoffeeScript and divvies it up into tagged
+are read by jison in the
+{Rewriter, INVERSES} = require './rewriter'
+
+
+
+
+ ¶
+
+ Import the helpers we need. + + +{count, starts, compact, last, repeat, invertLiterate,
+locationDataToString, throwSyntaxError} = require './helpers'
+
+
+
+
+ ¶
+
+ The Lexer Class+ +
+
+
+
+
+ ¶
+
+
+
+
+
+ ¶
+
+ The Lexer class reads a stream of CoffeeScript and divvies it up into tagged tokens. Some potential ambiguity in the grammar has been avoided by -pushing some extra smarts into the Lexer. | exports.Lexer = class Lexer |
tokenize is the Lexer's main method. Scan by attempting to match tokens +pushing some extra smarts into the Lexer. + + + + +exports.Lexer = class Lexer
+
+
+ ¶
+
+ tokenize is the Lexer's main method. Scan by attempting to match tokens one at a time, using a regular expression anchored at the start of the remaining code, or a custom recursive token-matching method (for interpolations). When the next token has been recorded, we move forward -within the code past the token, and begin again. +within the code past the token, and begin again. +Each tokenizing method is responsible for returning the number of characters -it has consumed. +it has consumed. +Before returning the token stream, run it through the Rewriter -unless explicitly asked not to. | tokenize: (code, opts = {}) ->
- @literate = opts.literate # Are we lexing literate CoffeeScript?
- @indent = 0 # The current indentation level.
- @indebt = 0 # The over-indentation at the current level.
- @outdebt = 0 # The under-outdentation at the current level.
- @indents = [] # The stack of all current indentation levels.
- @ends = [] # The stack for pairing up tokens.
- @tokens = [] # Stream of parsed tokens in the form `['TYPE', value, line]`.
-
- @chunkLine =
- opts.line or 0 # The start line for the current @chunk.
- @chunkColumn =
- opts.column or 0 # The start column of the current @chunk.
- code = @clean code # The stripped, cleaned original source code. |
At every position, run through this list of attempted matches, +unless explicitly asked not to. + + + + + tokenize: (code, opts = {}) ->
+ @literate = opts.literate # Are we lexing literate CoffeeScript?
+ @indent = 0 # The current indentation level.
+ @indebt = 0 # The over-indentation at the current level.
+ @outdebt = 0 # The under-outdentation at the current level.
+ @indents = [] # The stack of all current indentation levels.
+ @ends = [] # The stack for pairing up tokens.
+ @tokens = [] # Stream of parsed tokens in the form `['TYPE', value, location data]`.
+
+ @chunkLine =
+ opts.line or 0 # The start line for the current @chunk.
+ @chunkColumn =
+ opts.column or 0 # The start column of the current @chunk.
+ code = @clean code # The stripped, cleaned original source code.
+
+
+ ¶
+
+ At every position, run through this list of attempted matches,
short-circuiting if any of them succeed. Their order determines precedence:
- | i = 0
- while @chunk = code[i..]
- consumed = \
- @identifierToken() or
- @commentToken() or
- @whitespaceToken() or
- @lineToken() or
- @heredocToken() or
- @stringToken() or
- @numberToken() or
- @regexToken() or
- @jsToken() or
- @literalToken() |
Update position | [@chunkLine, @chunkColumn] = @getLineAndColumnFromChunk consumed
-
- i += consumed
-
- @closeIndentation()
- @error "missing #{tag}" if tag = @ends.pop()
- return @tokens if opts.rewrite is off
- (new Rewriter).rewrite @tokens |
Preprocess the code to remove leading and trailing whitespace, carriage -returns, etc. If we're lexing literate CoffeeScript, strip external Markdown -by removing all lines that aren't indented by at least four spaces or a tab. | clean: (code) ->
- code = code.slice(1) if code.charCodeAt(0) is BOM
- code = code.replace(/\r/g, '').replace TRAILING_SPACES, ''
- if WHITESPACE.test code
- code = "\n#{code}"
- @chunkLine--
- if @literate
- lines = for line in code.split('\n')
- if match = LITERATE.exec line
- line[match[0].length..]
- else
- '# ' + line
- code = lines.join '\n'
- code |
Tokenizers | |
Matches identifying literals: variables, keywords, method names, etc.
-Check to ensure that JavaScript reserved words aren't being used as
+ i = 0
+ while @chunk = code[i..]
+ consumed = \
+ @identifierToken() or
+ @commentToken() or
+ @whitespaceToken() or
+ @lineToken() or
+ @heredocToken() or
+ @stringToken() or
+ @numberToken() or
+ @regexToken() or
+ @jsToken() or
+ @literalToken()
+
+
+
+
+ ¶
+
+ Update position + + + [@chunkLine, @chunkColumn] = @getLineAndColumnFromChunk consumed
+
+ i += consumed
+
+ @closeIndentation()
+ @error "missing #{tag}" if tag = @ends.pop()
+ return @tokens if opts.rewrite is off
+ (new Rewriter).rewrite @tokens
+
+
+
+
+ ¶
+
+ Preprocess the code to remove leading and trailing whitespace, carriage +returns, etc. If we're lexing literate CoffeeScript, strip external Markdown +by removing all lines that aren't indented by at least four spaces or a tab. + + + clean: (code) ->
+ code = code.slice(1) if code.charCodeAt(0) is BOM
+ code = code.replace(/\r/g, '').replace TRAILING_SPACES, ''
+ if WHITESPACE.test code
+ code = "\n#{code}"
+ @chunkLine--
+ code = invertLiterate code if @literate
+ code
+
+
+
+
+ ¶
+
+ Tokenizers+ +
+
+
+
+
+ ¶
+
+
+
+
+
+ ¶
+
+ Matches identifying literals: variables, keywords, method names, etc.
+Check to ensure that JavaScript reserved words aren't being used as
identifiers. Because CoffeeScript reserves a handful of keywords that are
-allowed in JavaScript, we're careful not to tag them as keywords when
+allowed in JavaScript, we're careful not to tag them as keywords when
referenced as property names here, so you can still do | identifierToken: ->
- return 0 unless match = IDENTIFIER.exec @chunk
- [input, id, colon] = match |
Preserve length of id for location data | idLength = id.length
- poppedToken = undefined
-
- if id is 'own' and @tag() is 'FOR'
- @token 'OWN', id
- return id.length
- forcedIdentifier = colon or
- (prev = last @tokens) and (prev[0] in ['.', '?.', '::', '?::'] or
- not prev.spaced and prev[0] is '@')
- tag = 'IDENTIFIER'
-
- if not forcedIdentifier and (id in JS_KEYWORDS or id in COFFEE_KEYWORDS)
- tag = id.toUpperCase()
- if tag is 'WHEN' and @tag() in LINE_BREAK
- tag = 'LEADING_WHEN'
- else if tag is 'FOR'
- @seenFor = yes
- else if tag is 'UNLESS'
- tag = 'IF'
- else if tag in UNARY
- tag = 'UNARY'
- else if tag in RELATION
- if tag isnt 'INSTANCEOF' and @seenFor
- tag = 'FOR' + tag
- @seenFor = no
- else
- tag = 'RELATION'
- if @value() is '!'
- poppedToken = @tokens.pop()
- id = '!' + id
-
- if id in JS_FORBIDDEN
- if forcedIdentifier
- tag = 'IDENTIFIER'
- id = new String id
- id.reserved = yes
- else if id in RESERVED
- @error "reserved word \"#{id}\""
-
- unless forcedIdentifier
- id = COFFEE_ALIAS_MAP[id] if id in COFFEE_ALIASES
- tag = switch id
- when '!' then 'UNARY'
- when '==', '!=' then 'COMPARE'
- when '&&', '||' then 'LOGIC'
- when 'true', 'false' then 'BOOL'
- when 'break', 'continue' then 'STATEMENT'
- else tag
-
- tagToken = @token tag, id, 0, idLength
- if poppedToken
- [tagToken[2].first_line, tagToken[2].first_column] =
- [poppedToken[2].first_line, poppedToken[2].first_column]
- if colon
- colonOffset = input.lastIndexOf ':'
- @token ':', ':', colonOffset, colon.length
-
- input.length |
Matches numbers, including decimals, hex, and exponential notation. -Be careful not to interfere with ranges-in-progress. | numberToken: ->
- return 0 unless match = NUMBER.exec @chunk
- number = match[0]
- if /^0[BOX]/.test number
- @error "radix prefix '#{number}' must be lowercase"
- else if /E/.test(number) and not /^0x/.test number
- @error "exponential notation '#{number}' must be indicated with a lowercase 'e'"
- else if /^0\d*[89]/.test number
- @error "decimal literal '#{number}' must not be prefixed with '0'"
- else if /^0\d+/.test number
- @error "octal literal '#{number}' must be prefixed with '0o'"
- lexedLength = number.length
- if octalLiteral = /^0o([0-7]+)/.exec number
- number = '0x' + (parseInt octalLiteral[1], 8).toString 16
- if binaryLiteral = /^0b([01]+)/.exec number
- number = '0x' + (parseInt binaryLiteral[1], 2).toString 16
- @token 'NUMBER', number, 0, lexedLength
- lexedLength |
Matches strings, including multi-line strings. Ensures that quotation marks -are balanced within the string's contents, and within nested interpolations. | stringToken: ->
- switch @chunk.charAt 0
- when "'"
- return 0 unless match = SIMPLESTR.exec @chunk
- string = match[0]
- @token 'STRING', string.replace(MULTILINER, '\\\n'), 0, string.length
- when '"'
- return 0 unless string = @balancedString @chunk, '"'
- if 0 < string.indexOf '#{', 1
- @interpolateString string[1...-1], strOffset: 1, lexedLength: string.length
- else
- @token 'STRING', @escapeLines string, 0, string.length
- else
- return 0
- if octalEsc = /^(?:\\.|[^\\])*\\(?:0[0-7]|[1-7])/.test string
- @error "octal escape sequences #{string} are not allowed"
- string.length |
Matches heredocs, adjusting indentation to the correct level, as heredocs -preserve whitespace, but ignore indentation to the left. | heredocToken: ->
- return 0 unless match = HEREDOC.exec @chunk
- heredoc = match[0]
- quote = heredoc.charAt 0
- doc = @sanitizeHeredoc match[2], quote: quote, indent: null
- if quote is '"' and 0 <= doc.indexOf '#{'
- @interpolateString doc, heredoc: yes, strOffset: 3, lexedLength: heredoc.length
- else
- @token 'STRING', @makeString(doc, quote, yes), 0, heredoc.length
- heredoc.length |
Matches and consumes comments. | commentToken: ->
- return 0 unless match = @chunk.match COMMENT
- [comment, here] = match
- if here
- @token 'HERECOMMENT',
- (@sanitizeHeredoc here,
- herecomment: true, indent: Array(@indent + 1).join(' ')),
- 0, comment.length
- comment.length |
Matches JavaScript interpolated directly into the source via backticks. | jsToken: ->
- return 0 unless @chunk.charAt(0) is '`' and match = JSTOKEN.exec @chunk
- @token 'JS', (script = match[0])[1...-1], 0, script.length
- script.length |
Matches regular expression literals. Lexing regular expressions is difficult
+though identifierToken: ->
+ return 0 unless match = IDENTIFIER.exec @chunk
+ [input, id, colon] = match
+
+
+
+
+ ¶
+
+ Preserve length of id for location data + + + idLength = id.length
+ poppedToken = undefined
+
+ if id is 'own' and @tag() is 'FOR'
+ @token 'OWN', id
+ return id.length
+ forcedIdentifier = colon or
+ (prev = last @tokens) and (prev[0] in ['.', '?.', '::', '?::'] or
+ not prev.spaced and prev[0] is '@')
+ tag = 'IDENTIFIER'
+
+ if not forcedIdentifier and (id in JS_KEYWORDS or id in COFFEE_KEYWORDS)
+ tag = id.toUpperCase()
+ if tag is 'WHEN' and @tag() in LINE_BREAK
+ tag = 'LEADING_WHEN'
+ else if tag is 'FOR'
+ @seenFor = yes
+ else if tag is 'UNLESS'
+ tag = 'IF'
+ else if tag in UNARY
+ tag = 'UNARY'
+ else if tag in RELATION
+ if tag isnt 'INSTANCEOF' and @seenFor
+ tag = 'FOR' + tag
+ @seenFor = no
+ else
+ tag = 'RELATION'
+ if @value() is '!'
+ poppedToken = @tokens.pop()
+ id = '!' + id
+
+ if id in JS_FORBIDDEN
+ if forcedIdentifier
+ tag = 'IDENTIFIER'
+ id = new String id
+ id.reserved = yes
+ else if id in RESERVED
+ @error "reserved word \"#{id}\""
+
+ unless forcedIdentifier
+ id = COFFEE_ALIAS_MAP[id] if id in COFFEE_ALIASES
+ tag = switch id
+ when '!' then 'UNARY'
+ when '==', '!=' then 'COMPARE'
+ when '&&', '||' then 'LOGIC'
+ when 'true', 'false' then 'BOOL'
+ when 'break', 'continue' then 'STATEMENT'
+ else tag
+
+ tagToken = @token tag, id, 0, idLength
+ if poppedToken
+ [tagToken[2].first_line, tagToken[2].first_column] =
+ [poppedToken[2].first_line, poppedToken[2].first_column]
+ if colon
+ colonOffset = input.lastIndexOf ':'
+ @token ':', ':', colonOffset, colon.length
+
+ input.length
+
+
+
+
+ ¶
+
+ Matches numbers, including decimals, hex, and exponential notation. +Be careful not to interfere with ranges-in-progress. + + + numberToken: ->
+ return 0 unless match = NUMBER.exec @chunk
+ number = match[0]
+ if /^0[BOX]/.test number
+ @error "radix prefix '#{number}' must be lowercase"
+ else if /E/.test(number) and not /^0x/.test number
+ @error "exponential notation '#{number}' must be indicated with a lowercase 'e'"
+ else if /^0\d*[89]/.test number
+ @error "decimal literal '#{number}' must not be prefixed with '0'"
+ else if /^0\d+/.test number
+ @error "octal literal '#{number}' must be prefixed with '0o'"
+ lexedLength = number.length
+ if octalLiteral = /^0o([0-7]+)/.exec number
+ number = '0x' + parseInt(octalLiteral[1], 8).toString 16
+ if binaryLiteral = /^0b([01]+)/.exec number
+ number = '0x' + parseInt(binaryLiteral[1], 2).toString 16
+ @token 'NUMBER', number, 0, lexedLength
+ lexedLength
+
+
+
+
+ ¶
+
+ Matches strings, including multi-line strings. Ensures that quotation marks +are balanced within the string's contents, and within nested interpolations. + + + stringToken: ->
+ switch @chunk.charAt 0
+ when "'"
+ return 0 unless match = SIMPLESTR.exec @chunk
+ string = match[0]
+ @token 'STRING', string.replace(MULTILINER, '\\\n'), 0, string.length
+ when '"'
+ return 0 unless string = @balancedString @chunk, '"'
+ if 0 < string.indexOf '#{', 1
+ @interpolateString string[1...-1], strOffset: 1, lexedLength: string.length
+ else
+ @token 'STRING', @escapeLines string, 0, string.length
+ else
+ return 0
+ if octalEsc = /^(?:\\.|[^\\])*\\(?:0[0-7]|[1-7])/.test string
+ @error "octal escape sequences #{string} are not allowed"
+ string.length
+
+
+
+
+ ¶
+
+ Matches heredocs, adjusting indentation to the correct level, as heredocs +preserve whitespace, but ignore indentation to the left. + + + heredocToken: ->
+ return 0 unless match = HEREDOC.exec @chunk
+ heredoc = match[0]
+ quote = heredoc.charAt 0
+ doc = @sanitizeHeredoc match[2], quote: quote, indent: null
+ if quote is '"' and 0 <= doc.indexOf '#{'
+ @interpolateString doc, heredoc: yes, strOffset: 3, lexedLength: heredoc.length
+ else
+ @token 'STRING', @makeString(doc, quote, yes), 0, heredoc.length
+ heredoc.length
+
+
+
+
+ ¶
+
+ Matches and consumes comments. + + + commentToken: ->
+ return 0 unless match = @chunk.match COMMENT
+ [comment, here] = match
+ if here
+ @token 'HERECOMMENT',
+ (@sanitizeHeredoc here,
+ herecomment: true, indent: repeat ' ', @indent),
+ 0, comment.length
+ comment.length
+
+
+
+
+ ¶
+
+ Matches JavaScript interpolated directly into the source via backticks. + + + jsToken: ->
+ return 0 unless @chunk.charAt(0) is '`' and match = JSTOKEN.exec @chunk
+ @token 'JS', (script = match[0])[1...-1], 0, script.length
+ script.length
+
+
+ ¶
+
+ Matches regular expression literals. Lexing regular expressions is difficult to distinguish from division, so we borrow some basic heuristics from -JavaScript and Ruby. | regexToken: ->
- return 0 if @chunk.charAt(0) isnt '/'
- if match = HEREGEX.exec @chunk
- length = @heregexToken match
- return length
-
- prev = last @tokens
- return 0 if prev and (prev[0] in (if prev.spaced then NOT_REGEX else NOT_SPACED_REGEX))
- return 0 unless match = REGEX.exec @chunk
- [match, regex, flags] = match
- if regex[..1] is '/*' then @error 'regular expressions cannot begin with `*`'
- if regex is '//' then regex = '/(?:)/'
- @token 'REGEX', "#{regex}#{flags}", 0, match.length
- match.length |
Matches multiline extended regular expressions. | heregexToken: (match) ->
- [heregex, body, flags] = match
- if 0 > body.indexOf '#{'
- re = body.replace(HEREGEX_OMIT, '').replace(/\//g, '\\/')
- if re.match /^\*/ then @error 'regular expressions cannot begin with `*`'
- @token 'REGEX', "/#{ re or '(?:)' }/#{flags}", 0, heregex.length
- return heregex.length
- @token 'IDENTIFIER', 'RegExp', 0, 0
- @token 'CALL_START', '(', 0, 0
- tokens = []
- for token in @interpolateString(body, regex: yes)
- [tag, value] = token
- if tag is 'TOKENS'
- tokens.push value...
- else if tag is 'NEOSTRING'
- continue unless value = value.replace HEREGEX_OMIT, '' |
Convert NEOSTRING into STRING | value = value.replace /\\/g, '\\\\'
- token[0] = 'STRING'
- token[1] = @makeString(value, '"', yes)
- tokens.push token
- else
- @error "Unexpected #{tag}"
-
- prev = last @tokens
- plusToken = ['+', '+']
- plusToken[2] = prev[2] # Copy location data
- tokens.push plusToken |
Remove the extra "+" | tokens.pop()
-
- unless tokens[0]?[0] is 'STRING'
- @token 'STRING', '""', 0, 0
- @token '+', '+', 0, 0
- @tokens.push tokens...
-
- if flags |
Find the flags in the heregex | flagsOffset = heregex.lastIndexOf flags
- @token ',', ',', flagsOffset, 0
- @token 'STRING', '"' + flags + '"', flagsOffset, flags.length
-
- @token ')', ')', heregex.length-1, 0
- heregex.length |
Matches newlines, indents, and outdents, and determines which is which. +JavaScript and Ruby. + + + + + regexToken: ->
+ return 0 if @chunk.charAt(0) isnt '/'
+ if match = HEREGEX.exec @chunk
+ length = @heregexToken match
+ return length
+
+ prev = last @tokens
+ return 0 if prev and (prev[0] in (if prev.spaced then NOT_REGEX else NOT_SPACED_REGEX))
+ return 0 unless match = REGEX.exec @chunk
+ [match, regex, flags] = match
+ if regex[..1] is '/*' then @error 'regular expressions cannot begin with `*`'
+ if regex is '//' then regex = '/(?:)/'
+ @token 'REGEX', "#{regex}#{flags}", 0, match.length
+ match.length
+
+
+
+
+ ¶
+
+ Matches multiline extended regular expressions. + + + heregexToken: (match) ->
+ [heregex, body, flags] = match
+ if 0 > body.indexOf '#{'
+ re = body.replace(HEREGEX_OMIT, '').replace(/\//g, '\\/')
+ if re.match /^\*/ then @error 'regular expressions cannot begin with `*`'
+ @token 'REGEX', "/#{ re or '(?:)' }/#{flags}", 0, heregex.length
+ return heregex.length
+ @token 'IDENTIFIER', 'RegExp', 0, 0
+ @token 'CALL_START', '(', 0, 0
+ tokens = []
+ for token in @interpolateString(body, regex: yes)
+ [tag, value] = token
+ if tag is 'TOKENS'
+ tokens.push value...
+ else if tag is 'NEOSTRING'
+ continue unless value = value.replace HEREGEX_OMIT, ''
+
+
+
+
+ ¶
+
+ Convert NEOSTRING into STRING + + + value = value.replace /\\/g, '\\\\'
+ token[0] = 'STRING'
+ token[1] = @makeString(value, '"', yes)
+ tokens.push token
+ else
+ @error "Unexpected #{tag}"
+
+ prev = last @tokens
+ plusToken = ['+', '+']
+ plusToken[2] = prev[2] # Copy location data
+ tokens.push plusToken
+
+
+
+
+ ¶
+
+ Remove the extra "+" + + + tokens.pop()
+
+ unless tokens[0]?[0] is 'STRING'
+ @token 'STRING', '""', 0, 0
+ @token '+', '+', 0, 0
+ @tokens.push tokens...
+
+ if flags
+
+
+
+
+ ¶
+
+ Find the flags in the heregex + + + flagsOffset = heregex.lastIndexOf flags
+ @token ',', ',', flagsOffset, 0
+ @token 'STRING', '"' + flags + '"', flagsOffset, flags.length
+
+ @token ')', ')', heregex.length-1, 0
+ heregex.length
+
+
+ ¶
+
+ Matches newlines, indents, and outdents, and determines which is which. If we can detect that the current line is continued onto the the next line, -then the newline is suppressed: +then the newline is suppressed: +
-
+ .map( ... )
Keeps track of the level of indentation, because a single outdent token -can close multiple indents, so we need to know how far in we happen to be. | lineToken: ->
- return 0 unless match = MULTI_DENT.exec @chunk
- indent = match[0]
- @seenFor = no
- size = indent.length - 1 - indent.lastIndexOf '\n'
- noNewlines = @unfinished()
- if size - @indebt is @indent
- if noNewlines then @suppressNewlines() else @newlineToken 0
- return indent.length
-
- if size > @indent
- if noNewlines
- @indebt = size - @indent
- @suppressNewlines()
- return indent.length
- diff = size - @indent + @outdebt
- @token 'INDENT', diff, 0, indent.length
- @indents.push diff
- @ends.push 'OUTDENT'
- @outdebt = @indebt = 0
- else
- @indebt = 0
- @outdentToken @indent - size, noNewlines, indent.length
- @indent = size
- indent.length |
Record an outdent token or multiple tokens, if we happen to be moving back -inwards past several recorded indents. | outdentToken: (moveOut, noNewlines, outdentLength) ->
- while moveOut > 0
- len = @indents.length - 1
- if @indents[len] is undefined
- moveOut = 0
- else if @indents[len] is @outdebt
- moveOut -= @outdebt
- @outdebt = 0
- else if @indents[len] < @outdebt
- @outdebt -= @indents[len]
- moveOut -= @indents[len]
- else
- dent = @indents.pop() + @outdebt
- moveOut -= dent
- @outdebt = 0
- @pair 'OUTDENT'
- @token 'OUTDENT', dent, 0, outdentLength
- @outdebt -= moveOut if dent
- @tokens.pop() while @value() is ';'
-
- @token 'TERMINATOR', '\n', outdentLength, 0 unless @tag() is 'TERMINATOR' or noNewlines
- this |
Matches and consumes non-meaningful whitespace. Tag the previous token -as being "spaced", because there are some cases where it makes a difference. | whitespaceToken: ->
- return 0 unless (match = WHITESPACE.exec @chunk) or
- (nline = @chunk.charAt(0) is '\n')
- prev = last @tokens
- prev[if match then 'spaced' else 'newLine'] = true if prev
- if match then match[0].length else 0 |
Generate a newline token. Consecutive newlines get merged together. | newlineToken: (offset) ->
- @tokens.pop() while @value() is ';'
- @token 'TERMINATOR', '\n', offset, 0 unless @tag() is 'TERMINATOR'
- this |
Use a | suppressNewlines: ->
- @tokens.pop() if @value() is '\\'
- this |
We treat all other single characters as a token. E.g.: lineToken: ->
+ return 0 unless match = MULTI_DENT.exec @chunk
+ indent = match[0]
+ @seenFor = no
+ size = indent.length - 1 - indent.lastIndexOf '\n'
+ noNewlines = @unfinished()
+ if size - @indebt is @indent
+ if noNewlines then @suppressNewlines() else @newlineToken 0
+ return indent.length
+
+ if size > @indent
+ if noNewlines
+ @indebt = size - @indent
+ @suppressNewlines()
+ return indent.length
+ diff = size - @indent + @outdebt
+ @token 'INDENT', diff, indent.length - size, size
+ @indents.push diff
+ @ends.push 'OUTDENT'
+ @outdebt = @indebt = 0
+ else
+ @indebt = 0
+ @outdentToken @indent - size, noNewlines, indent.length
+ @indent = size
+ indent.length
+
+
+
+
+ ¶
+
+ Record an outdent token or multiple tokens, if we happen to be moving back +inwards past several recorded indents. + + + outdentToken: (moveOut, noNewlines, outdentLength) ->
+ while moveOut > 0
+ len = @indents.length - 1
+ if @indents[len] is undefined
+ moveOut = 0
+ else if @indents[len] is @outdebt
+ moveOut -= @outdebt
+ @outdebt = 0
+ else if @indents[len] < @outdebt
+ @outdebt -= @indents[len]
+ moveOut -= @indents[len]
+ else
+ dent = @indents.pop() + @outdebt
+ moveOut -= dent
+ @outdebt = 0
+ @pair 'OUTDENT'
+ @token 'OUTDENT', dent, 0, outdentLength
+ @outdebt -= moveOut if dent
+ @tokens.pop() while @value() is ';'
+
+ @token 'TERMINATOR', '\n', outdentLength, 0 unless @tag() is 'TERMINATOR' or noNewlines
+ this
+
+
+
+
+ ¶
+
+ Matches and consumes non-meaningful whitespace. Tag the previous token +as being "spaced", because there are some cases where it makes a difference. + + + whitespaceToken: ->
+ return 0 unless (match = WHITESPACE.exec @chunk) or
+ (nline = @chunk.charAt(0) is '\n')
+ prev = last @tokens
+ prev[if match then 'spaced' else 'newLine'] = true if prev
+ if match then match[0].length else 0
+
+
+
+
+ ¶
+
+ Generate a newline token. Consecutive newlines get merged together. + + + newlineToken: (offset) ->
+ @tokens.pop() while @value() is ';'
+ @token 'TERMINATOR', '\n', offset, 0 unless @tag() is 'TERMINATOR'
+ this
+
+
+
+
+ ¶
+
+ Use a suppressNewlines: ->
+ @tokens.pop() if @value() is '\\'
+ this
+
+
+ ¶
+
+ We treat all other single characters as a token. E.g.: | literalToken: ->
- if match = OPERATOR.exec @chunk
- [value] = match
- @tagParameters() if CODE.test value
- else
- value = @chunk.charAt 0
- tag = value
- prev = last @tokens
- if value is '=' and prev
- if not prev[1].reserved and prev[1] in JS_FORBIDDEN
- @error "reserved word \"#{@value()}\" can't be assigned"
- if prev[1] in ['||', '&&']
- prev[0] = 'COMPOUND_ASSIGN'
- prev[1] += '='
- return value.length
- if value is ';'
- @seenFor = no
- tag = 'TERMINATOR'
- else if value in MATH then tag = 'MATH'
- else if value in COMPARE then tag = 'COMPARE'
- else if value in COMPOUND_ASSIGN then tag = 'COMPOUND_ASSIGN'
- else if value in UNARY then tag = 'UNARY'
- else if value in SHIFT then tag = 'SHIFT'
- else if value in LOGIC or value is '?' and prev?.spaced then tag = 'LOGIC'
- else if prev and not prev.spaced
- if value is '(' and prev[0] in CALLABLE
- prev[0] = 'FUNC_EXIST' if prev[0] is '?'
- tag = 'CALL_START'
- else if value is '[' and prev[0] in INDEXABLE
- tag = 'INDEX_START'
- switch prev[0]
- when '?' then prev[0] = 'INDEX_SOAK'
- switch value
- when '(', '{', '[' then @ends.push INVERSES[value]
- when ')', '}', ']' then @pair value
- @token tag, value
- value.length |
Token Manipulators | |
Sanitize a heredoc or herecomment by -erasing all external indentation on the left-hand side. | sanitizeHeredoc: (doc, options) ->
- {indent, herecomment} = options
- if herecomment
- if HEREDOC_ILLEGAL.test doc
- @error "block comment cannot contain \"*/\", starting"
- return doc if doc.indexOf('\n') < 0
- else
- while match = HEREDOC_INDENT.exec doc
- attempt = match[1]
- indent = attempt if indent is null or 0 < attempt.length < indent.length
- doc = doc.replace /// \n #{indent} ///g, '\n' if indent
- doc = doc.replace /\n# \n/g, '\n\n' if @literate
- doc = doc.replace /^\n/, '' unless herecomment
- doc |
A source of ambiguity in our grammar used to be parameter lists in function +parentheses that indicate a method call from regular parentheses, and so on. + + + + + literalToken: ->
+ if match = OPERATOR.exec @chunk
+ [value] = match
+ @tagParameters() if CODE.test value
+ else
+ value = @chunk.charAt 0
+ tag = value
+ prev = last @tokens
+ if value is '=' and prev
+ if not prev[1].reserved and prev[1] in JS_FORBIDDEN
+ @error "reserved word \"#{@value()}\" can't be assigned"
+ if prev[1] in ['||', '&&']
+ prev[0] = 'COMPOUND_ASSIGN'
+ prev[1] += '='
+ return value.length
+ if value is ';'
+ @seenFor = no
+ tag = 'TERMINATOR'
+ else if value in MATH then tag = 'MATH'
+ else if value in COMPARE then tag = 'COMPARE'
+ else if value in COMPOUND_ASSIGN then tag = 'COMPOUND_ASSIGN'
+ else if value in UNARY then tag = 'UNARY'
+ else if value in SHIFT then tag = 'SHIFT'
+ else if value in LOGIC or value is '?' and prev?.spaced then tag = 'LOGIC'
+ else if prev and not prev.spaced
+ if value is '(' and prev[0] in CALLABLE
+ prev[0] = 'FUNC_EXIST' if prev[0] is '?'
+ tag = 'CALL_START'
+ else if value is '[' and prev[0] in INDEXABLE
+ tag = 'INDEX_START'
+ switch prev[0]
+ when '?' then prev[0] = 'INDEX_SOAK'
+ switch value
+ when '(', '{', '[' then @ends.push INVERSES[value]
+ when ')', '}', ']' then @pair value
+ @token tag, value
+ value.length
+
+
+
+
+ ¶
+
+ Token Manipulators+ +
+
+
+
+
+ ¶
+
+
+
+
+
+
+
+ ¶
+
+ Sanitize a heredoc or herecomment by +erasing all external indentation on the left-hand side. + + + sanitizeHeredoc: (doc, options) ->
+ {indent, herecomment} = options
+ if herecomment
+ if HEREDOC_ILLEGAL.test doc
+ @error "block comment cannot contain \"*/\", starting"
+ return doc if doc.indexOf('\n') < 0
+ else
+ while match = HEREDOC_INDENT.exec doc
+ attempt = match[1]
+ indent = attempt if indent is null or 0 < attempt.length < indent.length
+ doc = doc.replace /// \n #{indent} ///g, '\n' if indent
+ doc = doc.replace /^\n/, '' unless herecomment
+ doc
+
+
+ ¶
+
+ A source of ambiguity in our grammar used to be parameter lists in function definitions versus argument lists in function calls. Walk backwards, tagging -parameters specially in order to make things easier for the parser. | tagParameters: ->
- return this if @tag() isnt ')'
- stack = []
- {tokens} = this
- i = tokens.length
- tokens[--i][0] = 'PARAM_END'
- while tok = tokens[--i]
- switch tok[0]
- when ')'
- stack.push tok
- when '(', 'CALL_START'
- if stack.length then stack.pop()
- else if tok[0] is '('
- tok[0] = 'PARAM_START'
- return this
- else return this
- this |
Close up all remaining open blocks at the end of the file. | closeIndentation: ->
- @outdentToken @indent |
Matches a balanced group such as a single or double-quoted string. Pass in +parameters specially in order to make things easier for the parser. + + + + + tagParameters: ->
+ return this if @tag() isnt ')'
+ stack = []
+ {tokens} = this
+ i = tokens.length
+ tokens[--i][0] = 'PARAM_END'
+ while tok = tokens[--i]
+ switch tok[0]
+ when ')'
+ stack.push tok
+ when '(', 'CALL_START'
+ if stack.length then stack.pop()
+ else if tok[0] is '('
+ tok[0] = 'PARAM_START'
+ return this
+ else return this
+ this
+
+
+
+
+ ¶
+
+ Close up all remaining open blocks at the end of the file. + + + closeIndentation: ->
+ @outdentToken @indent
+
+
+ ¶
+
+ Matches a balanced group such as a single or double-quoted string. Pass in a series of delimiters, all of which must be nested correctly within the contents of the string. This method allows us to have strings within -interpolations within strings, ad infinitum. | balancedString: (str, end) ->
- continueCount = 0
- stack = [end]
- for i in [1...str.length]
- if continueCount
- --continueCount
- continue
- switch letter = str.charAt i
- when '\\'
- ++continueCount
- continue
- when end
- stack.pop()
- unless stack.length
- return str[0..i]
- end = stack[stack.length - 1]
- continue
- if end is '}' and letter in ['"', "'"]
- stack.push end = letter
- else if end is '}' and letter is '/' and match = (HEREGEX.exec(str[i..]) or REGEX.exec(str[i..]))
- continueCount += match[0].length - 1
- else if end is '}' and letter is '{'
- stack.push end = '}'
- else if end is '"' and prev is '#' and letter is '{'
- stack.push end = '}'
- prev = letter
- @error "missing #{ stack.pop() }, starting" |
Expand variables and expressions inside double-quoted strings using -Ruby-like notation for substitution of arbitrary expressions. - -
+interpolations within strings, ad infinitum.
+
+
+
+
+ balancedString: (str, end) ->
+ continueCount = 0
+ stack = [end]
+ for i in [1...str.length]
+ if continueCount
+ --continueCount
+ continue
+ switch letter = str.charAt i
+ when '\\'
+ ++continueCount
+ continue
+ when end
+ stack.pop()
+ unless stack.length
+ return str[0..i]
+ end = stack[stack.length - 1]
+ continue
+ if end is '}' and letter in ['"', "'"]
+ stack.push end = letter
+ else if end is '}' and letter is '/' and match = (HEREGEX.exec(str[i..]) or REGEX.exec(str[i..]))
+ continueCount += match[0].length - 1
+ else if end is '}' and letter is '{'
+ stack.push end = '}'
+ else if end is '"' and prev is '#' and letter is '{'
+ stack.push end = '}'
+ prev = letter
+ @error "missing #{ stack.pop() }, starting"
+
+
+ ¶
+
+ Expand variables and expressions inside double-quoted strings using +Ruby-like notation for substitution of arbitrary expressions. + +
If it encounters an interpolation, this method will recursively create a new Lexer, tokenize the interpolated contents, and merge them into the -token stream. +token stream. +
| interpolateString: (str, options = {}) ->
- {heredoc, regex, offsetInChunk, strOffset, lexedLength} = options
- offsetInChunk = offsetInChunk || 0
- strOffset = strOffset || 0
- lexedLength = lexedLength || str.length |
Clip leading \n from heredoc | if heredoc and str.length > 0 and str[0] == '\n'
- str = str[1...]
- strOffset++ |
Parse the string. | tokens = []
- pi = 0
- i = -1
- while letter = str.charAt i += 1
- if letter is '\\'
- i += 1
- continue
- unless letter is '#' and str.charAt(i+1) is '{' and
- (expr = @balancedString str[i + 1..], '}')
- continue |
NEOSTRING is a fake token. This will be converted to a string below. | tokens.push @makeToken('NEOSTRING', str[pi...i], strOffset + pi) if pi < i
- inner = expr[1...-1]
- if inner.length
- [line, column] = @getLineAndColumnFromChunk(strOffset + i + 1)
- nested = new Lexer().tokenize inner, line: line, column: column, rewrite: off
- popped = nested.pop()
- popped = nested.shift() if nested[0]?[0] is 'TERMINATOR'
- if len = nested.length
- if len > 1
- nested.unshift @makeToken '(', '(', strOffset + i + 1, 0
- nested.push @makeToken ')', ')', strOffset + i + 1 + inner.length, 0 |
Push a fake 'TOKENS' token, which will get turned into real tokens below. | tokens.push ['TOKENS', nested]
- i += expr.length
- pi = i + 1
- tokens.push @makeToken('NEOSTRING', str[pi..], strOffset + pi) if i > pi < str.length |
If regex, then return now and let the regex code deal with all these fake tokens | return tokens if regex |
If we didn't find any tokens, then just return an empty string. | return @token 'STRING', '""', offsetInChunk, lexedLength unless tokens.length |
If the first token is not a string, add a fake empty string to the beginning. | tokens.unshift @makeToken('NEOSTRING', '', offsetInChunk) unless tokens[0][0] is 'NEOSTRING'
-
- @token '(', '(', offsetInChunk, 0 if interpolated = tokens.length > 1 |
Push all the tokens | for token, i in tokens
- [tag, value] = token
- if i |
Create a 0-length "+" token. | plusToken = @token '+', '+' if i
- locationToken = if tag == 'TOKENS' then value[0] else token
- plusToken[2] =
- first_line: locationToken[2].first_line
- first_column: locationToken[2].first_column
- last_line: locationToken[2].first_line
- last_column: locationToken[2].first_column
- if tag is 'TOKENS' |
Push all the tokens in the fake 'TOKENS' token. These already have -sane location data. | @tokens.push value...
- else if tag is 'NEOSTRING' |
Convert NEOSTRING into STRING | token[0] = 'STRING'
- token[1] = @makeString value, '"', heredoc
- @tokens.push token
- else
- @error "Unexpected #{tag}"
- @token ')', ')', offsetInChunk + lexedLength, 0 if interpolated
- tokens |
Pairs up a closing token, ensuring that all listed pairs of tokens are -correctly balanced throughout the course of the token stream. | pair: (tag) ->
- unless tag is wanted = last @ends
- @error "unmatched #{tag}" unless 'OUTDENT' is wanted |
Auto-close INDENT to support syntax like this: + + + + + interpolateString: (str, options = {}) ->
+ {heredoc, regex, offsetInChunk, strOffset, lexedLength} = options
+ offsetInChunk = offsetInChunk || 0
+ strOffset = strOffset || 0
+ lexedLength = lexedLength || str.length
+
+
+
+
+ ¶
+
+ Clip leading \n from heredoc + + + if heredoc and str.length > 0 and str[0] == '\n'
+ str = str[1...]
+ strOffset++
+
+
+
+
+ ¶
+
+ Parse the string. + + + tokens = []
+ pi = 0
+ i = -1
+ while letter = str.charAt i += 1
+ if letter is '\\'
+ i += 1
+ continue
+ unless letter is '#' and str.charAt(i+1) is '{' and
+ (expr = @balancedString str[i + 1..], '}')
+ continue
+
+
+
+
+ ¶
+
+ NEOSTRING is a fake token. This will be converted to a string below. + + tokens.push @makeToken('NEOSTRING', str[pi...i], strOffset + pi) if pi < i
+ inner = expr[1...-1]
+ if inner.length
+ [line, column] = @getLineAndColumnFromChunk(strOffset + i + 1)
+ nested = new Lexer().tokenize inner, line: line, column: column, rewrite: off
+ popped = nested.pop()
+ popped = nested.shift() if nested[0]?[0] is 'TERMINATOR'
+ if len = nested.length
+ if len > 1
+ nested.unshift @makeToken '(', '(', strOffset + i + 1, 0
+ nested.push @makeToken ')', ')', strOffset + i + 1 + inner.length, 0
+
+
+
+
+ ¶
+
+ Push a fake 'TOKENS' token, which will get turned into real tokens below. + + + tokens.push ['TOKENS', nested]
+ i += expr.length
+ pi = i + 1
+ tokens.push @makeToken('NEOSTRING', str[pi..], strOffset + pi) if i > pi < str.length
+
+
+
+
+ ¶
+
+ If regex, then return now and let the regex code deal with all these fake tokens + + + return tokens if regex
+
+
+
+
+ ¶
+
+ If we didn't find any tokens, then just return an empty string. + + + return @token 'STRING', '""', offsetInChunk, lexedLength unless tokens.length
+
+
+
+
+ ¶
+
+ If the first token is not a string, add a fake empty string to the beginning. + + + tokens.unshift @makeToken('NEOSTRING', '', offsetInChunk) unless tokens[0][0] is 'NEOSTRING'
+
+ @token '(', '(', offsetInChunk, 0 if interpolated = tokens.length > 1
+
+
+
+
+ ¶
+
+ Push all the tokens + + + for token, i in tokens
+ [tag, value] = token
+ if i
+
+
+
+
+ ¶
+
+ Create a 0-length "+" token. + + + plusToken = @token '+', '+' if i
+ locationToken = if tag == 'TOKENS' then value[0] else token
+ plusToken[2] =
+ first_line: locationToken[2].first_line
+ first_column: locationToken[2].first_column
+ last_line: locationToken[2].first_line
+ last_column: locationToken[2].first_column
+ if tag is 'TOKENS'
+
+
+
+
+ ¶
+
+ Push all the tokens in the fake 'TOKENS' token. These already have +sane location data. + + + @tokens.push value...
+ else if tag is 'NEOSTRING'
+
+
+
+
+ ¶
+
+ Convert NEOSTRING into STRING + + + token[0] = 'STRING'
+ token[1] = @makeString value, '"', heredoc
+ @tokens.push token
+ else
+ @error "Unexpected #{tag}"
+ if interpolated
+ rparen = @makeToken ')', ')', offsetInChunk + lexedLength, 0
+ rparen.stringEnd = true
+ @tokens.push rparen
+ tokens
+
+
+
+
+ ¶
+
+ Pairs up a closing token, ensuring that all listed pairs of tokens are +correctly balanced throughout the course of the token stream. + + + pair: (tag) ->
+ unless tag is wanted = last @ends
+ @error "unmatched #{tag}" unless 'OUTDENT' is wanted | @indent -= size = last @indents
- @outdentToken size, true
- return @pair tag
- @ends.pop() |
Helpers | |
Returns the line and column number from an offset into the current chunk. - -
| getLineAndColumnFromChunk: (offset) ->
- if offset is 0
- return [@chunkLine, @chunkColumn]
-
- if offset >= @chunk.length
- string = @chunk
- else
- string = @chunk[..offset-1]
-
- lineCount = count string, '\n'
-
- column = @chunkColumn
- if lineCount > 0
- lines = string.split '\n'
- column = (last lines).length
- else
- column += string.length
-
- [@chunkLine + lineCount, column] |
Same as "token", exception this just returns the token without adding it -to the results. | makeToken: (tag, value, offsetInChunk = 0, length = value.length) ->
- locationData = {}
- [locationData.first_line, locationData.first_column] =
- @getLineAndColumnFromChunk offsetInChunk |
Use length - 1 for the final offset - we're supplying the lastline and the lastcolumn, -so if lastcolumn == firstcolumn, then we're looking at a character of length 1. | lastCharacter = Math.max 0, length - 1
- [locationData.last_line, locationData.last_column] =
- @getLineAndColumnFromChunk offsetInChunk + (length - 1)
-
- token = [tag, value, locationData]
-
- token |
Add a token to the results. + el.hide()) + + + + @indent -= size = last @indents
+ @outdentToken size, true
+ return @pair tag
+ @ends.pop()
+
+
+
+
+ ¶
+
+ Helpers+ +
+
+
+
+
+ ¶
+
+
+
+
+
+
+
+ ¶
+
+ Returns the line and column number from an offset into the current chunk. + + +
getLineAndColumnFromChunk: (offset) ->
+ if offset is 0
+ return [@chunkLine, @chunkColumn]
+
+ if offset >= @chunk.length
+ string = @chunk
+ else
+ string = @chunk[..offset-1]
+
+ lineCount = count string, '\n'
+
+ column = @chunkColumn
+ if lineCount > 0
+ lines = string.split '\n'
+ column = last(lines).length
+ else
+ column += string.length
+
+ [@chunkLine + lineCount, column]
+
+
+
+
+ ¶
+
+ Same as "token", exception this just returns the token without adding it +to the results. + + + makeToken: (tag, value, offsetInChunk = 0, length = value.length) ->
+ locationData = {}
+ [locationData.first_line, locationData.first_column] =
+ @getLineAndColumnFromChunk offsetInChunk
+
+
+
+
+ ¶
+
+ Use length - 1 for the final offset - we're supplying the last_line and the last_column, +so if last_column == first_column, then we're looking at a character of length 1. + + + lastCharacter = Math.max 0, length - 1
+ [locationData.last_line, locationData.last_column] =
+ @getLineAndColumnFromChunk offsetInChunk + lastCharacter
+
+ token = [tag, value, locationData]
+
+ token
+
+
+ ¶
+
+ Add a token to the results.
Returns the new token. | token: (tag, value, offsetInChunk, length) ->
- token = @makeToken tag, value, offsetInChunk, length
- @tokens.push token
- token |
Peek at a tag in the current token stream. | tag: (index, tag) ->
- (tok = last @tokens, index) and if tag then tok[0] = tag else tok[0] |
Peek at a value in the current token stream. | value: (index, val) ->
- (tok = last @tokens, index) and if val then tok[1] = val else tok[1] |
Are we in the midst of an unfinished expression? | unfinished: ->
- LINE_CONTINUER.test(@chunk) or
- @tag() in ['\\', '.', '?.', '?::', 'UNARY', 'MATH', '+', '-', 'SHIFT', 'RELATION'
- 'COMPARE', 'LOGIC', 'THROW', 'EXTENDS'] |
Converts newlines for string literals. | escapeLines: (str, heredoc) ->
- str.replace MULTILINER, if heredoc then '\\n' else '' |
Constructs a string token by escaping quotes and newlines. | makeString: (body, quote, heredoc) ->
- return quote + quote unless body
- body = body.replace /\\([\s\S])/g, (match, contents) ->
- if contents in ['\n', quote] then contents else match
- body = body.replace /// #{quote} ///g, '\\$&'
- quote + @escapeLines(body, heredoc) + quote |
Throws a syntax error on the current | error: (message) -> |
TODO: Are there some cases we could improve the error line number by -passing the offset in the chunk where the error happened? | throw SyntaxError "#{message} on line #{ @chunkLine + 1 }" |
Constants | |
Keywords that CoffeeScript shares in common with JavaScript. | JS_KEYWORDS = [
- 'true', 'false', 'null', 'this'
- 'new', 'delete', 'typeof', 'in', 'instanceof'
- 'return', 'throw', 'break', 'continue', 'debugger'
- 'if', 'else', 'switch', 'for', 'while', 'do', 'try', 'catch', 'finally'
- 'class', 'extends', 'super'
-] |
CoffeeScript-only keywords. | COFFEE_KEYWORDS = ['undefined', 'then', 'unless', 'until', 'loop', 'of', 'by', 'when']
-
-COFFEE_ALIAS_MAP =
- and : '&&'
- or : '||'
- is : '=='
- isnt : '!='
- not : '!'
- yes : 'true'
- no : 'false'
- on : 'true'
- off : 'false'
-
-COFFEE_ALIASES = (key for key of COFFEE_ALIAS_MAP)
-COFFEE_KEYWORDS = COFFEE_KEYWORDS.concat COFFEE_ALIASES |
The list of keywords that are reserved by JavaScript, but not used, or are
+not specified, the length of Returns the new token. + + + + + token: (tag, value, offsetInChunk, length) ->
+ token = @makeToken tag, value, offsetInChunk, length
+ @tokens.push token
+ token
+
+
+
+
+ ¶
+
+ Peek at a tag in the current token stream. + + + tag: (index, tag) ->
+ (tok = last @tokens, index) and if tag then tok[0] = tag else tok[0]
+
+
+
+
+ ¶
+
+ Peek at a value in the current token stream. + + + value: (index, val) ->
+ (tok = last @tokens, index) and if val then tok[1] = val else tok[1]
+
+
+
+
+ ¶
+
+ Are we in the midst of an unfinished expression? + + + unfinished: ->
+ LINE_CONTINUER.test(@chunk) or
+ @tag() in ['\\', '.', '?.', '?::', 'UNARY', 'MATH', '+', '-', 'SHIFT', 'RELATION'
+ 'COMPARE', 'LOGIC', 'THROW', 'EXTENDS']
+
+
+
+
+ ¶
+
+ Converts newlines for string literals. + + + escapeLines: (str, heredoc) ->
+ str.replace MULTILINER, if heredoc then '\\n' else ''
+
+
+
+
+ ¶
+
+ Constructs a string token by escaping quotes and newlines. + + + makeString: (body, quote, heredoc) ->
+ return quote + quote unless body
+ body = body.replace /\\([\s\S])/g, (match, contents) ->
+ if contents in ['\n', quote] then contents else match
+ body = body.replace /// #{quote} ///g, '\\$&'
+ quote + @escapeLines(body, heredoc) + quote
+
+
+
+
+ ¶
+
+ Throws a compiler error on the current position. + + + error: (message) ->
+
+
+
+
+ ¶
+
+ TODO: Are there some cases we could improve the error line number by +passing the offset in the chunk where the error happened? + + + throwSyntaxError message, first_line: @chunkLine, first_column: @chunkColumn
+
+
+
+
+ ¶
+
+ Constants+ +
+
+
+
+
+ ¶
+
+
+
+
+
+
+
+ ¶
+
+ Keywords that CoffeeScript shares in common with JavaScript. + + +JS_KEYWORDS = [
+ 'true', 'false', 'null', 'this'
+ 'new', 'delete', 'typeof', 'in', 'instanceof'
+ 'return', 'throw', 'break', 'continue', 'debugger'
+ 'if', 'else', 'switch', 'for', 'while', 'do', 'try', 'catch', 'finally'
+ 'class', 'extends', 'super'
+]
+
+
+
+
+ ¶
+
+ CoffeeScript-only keywords. + + +COFFEE_KEYWORDS = ['undefined', 'then', 'unless', 'until', 'loop', 'of', 'by', 'when']
+
+COFFEE_ALIAS_MAP =
+ and : '&&'
+ or : '||'
+ is : '=='
+ isnt : '!='
+ not : '!'
+ yes : 'true'
+ no : 'false'
+ on : 'true'
+ off : 'false'
+
+COFFEE_ALIASES = (key for key of COFFEE_ALIAS_MAP)
+COFFEE_KEYWORDS = COFFEE_KEYWORDS.concat COFFEE_ALIASES
+
+
+ ¶
+
+ The list of keywords that are reserved by JavaScript, but not used, or are used by CoffeeScript internally. We throw an error when these are encountered, -to avoid having a JavaScript error at runtime. | RESERVED = [
- 'case', 'default', 'function', 'var', 'void', 'with', 'const', 'let', 'enum'
- 'export', 'import', 'native', '__hasProp', '__extends', '__slice', '__bind'
- '__indexOf', 'implements', 'interface', 'package', 'private', 'protected'
- 'public', 'static', 'yield'
-]
+to avoid having a JavaScript error at runtime.
+
+
+ RESERVED = [
+ 'case', 'default', 'function', 'var', 'void', 'with', 'const', 'let', 'enum'
+ 'export', 'import', 'native', '__hasProp', '__extends', '__slice', '__bind'
+ '__indexOf', 'implements', 'interface', 'package', 'private', 'protected'
+ 'public', 'static', 'yield'
+]
+
+STRICT_PROSCRIBED = ['arguments', 'eval']
+
+
+
+
+ ¶
+
+ The superset of both JavaScript keywords and reserved words, none of which may +be used as identifiers or properties. + + +JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED).concat(STRICT_PROSCRIBED)
-STRICT_PROSCRIBED = ['arguments', 'eval'] |
The superset of both JavaScript keywords and reserved words, none of which may -be used as identifiers or properties. | JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED).concat(STRICT_PROSCRIBED)
+exports.RESERVED = RESERVED.concat(JS_KEYWORDS).concat(COFFEE_KEYWORDS).concat(STRICT_PROSCRIBED)
+exports.STRICT_PROSCRIBED = STRICT_PROSCRIBED
+
+
+ ¶
+
+ The character code of the nasty Microsoft madness otherwise known as the BOM. + -exports.RESERVED = RESERVED.concat(JS_KEYWORDS).concat(COFFEE_KEYWORDS).concat(STRICT_PROSCRIBED) -exports.STRICT_PROSCRIBED = STRICT_PROSCRIBED |
The character code of the nasty Microsoft madness otherwise known as the BOM. | BOM = 65279 |
Token matching regexes. | IDENTIFIER = /// ^
- ( [$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]* )
- ( [^\n\S]* : (?!:) )? # Is this a property name?
-///
+ BOM = 65279
+
+
+
+
+ ¶
+
+ Token matching regexes. + -NUMBER = /// - ^ 0b[01]+ | # binary - ^ 0o[0-7]+ | # octal - ^ 0x[\da-f]+ | # hex - ^ \d*\.?\d+ (?:e[+-]?\d+)? # decimal -///i +IDENTIFIER = /// ^
+ ( [$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]* )
+ ( [^\n\S]* : (?!:) )? # Is this a property name?
+///
-HEREDOC = /// ^ ("""|''') ([\s\S]*?) (?:\n[^\n\S]*)? \1 ///
+NUMBER = ///
+ ^ 0b[01]+ | # binary
+ ^ 0o[0-7]+ | # octal
+ ^ 0x[\da-f]+ | # hex
+ ^ \d*\.?\d+ (?:e[+-]?\d+)? # decimal
+///i
-OPERATOR = /// ^ (
- ?: [-=]> # function
- | [-+*/%<>&|^!?=]= # compound assign / compare
- | >>>=? # zero-fill right shift
- | ([-+:])\1 # doubles
- | ([&|<>])\2=? # logic / shift
- | \?(\.|::) # soak access
- | \.{2,3} # range or splat
-) ///
+HEREDOC = /// ^ ("""|''') ([\s\S]*?) (?:\n[^\n\S]*)? \1 ///
-WHITESPACE = /^[^\n\S]+/
+OPERATOR = /// ^ (
+ ?: [-=]> # function
+ | [-+*/%<>&|^!?=]= # compound assign / compare
+ | >>>=? # zero-fill right shift
+ | ([-+:])\1 # doubles
+ | ([&|<>])\2=? # logic / shift
+ | \?(\.|::) # soak access
+ | \.{2,3} # range or splat
+) ///
-COMMENT = /^###([^#][\s\S]*?)(?:###[^\n\S]*|(?:###)$)|^(?:\s*#(?!##[^#]).*)+/
+WHITESPACE = /^[^\n\S]+/
-LITERATE = /^([ ]{4}|\t)/
+COMMENT = /^###([^#][\s\S]*?)(?:###[^\n\S]*|(?:###)$)|^(?:\s*#(?!##[^#]).*)+/
-CODE = /^[-=]>/
+CODE = /^[-=]>/
-MULTI_DENT = /^(?:\n[^\n\S]*)+/
+MULTI_DENT = /^(?:\n[^\n\S]*)+/
-SIMPLESTR = /^'[^\\']*(?:\\.[^\\']*)*'/
+SIMPLESTR = /^'[^\\']*(?:\\.[^\\']*)*'/
-JSTOKEN = /^`[^\\`]*(?:\\.[^\\`]*)*`/ |
Regex-matching-regexes. | REGEX = /// ^
- (/ (?! [\s=] ) # disallow leading whitespace or equals signs
- [^ [ / \n \\ ]* # every other thing
- (?:
- (?: \\[\s\S] # anything escaped
- | \[ # character class
- [^ \] \n \\ ]*
- (?: \\[\s\S] [^ \] \n \\ ]* )*
- ]
- ) [^ [ / \n \\ ]*
- )*
- /) ([imgy]{0,4}) (?!\w)
-///
+JSTOKEN = /^`[^\\`]*(?:\\.[^\\`]*)*`/
+
+
+
+
+ ¶
+
+ Regex-matching-regexes. + -HEREGEX = /// ^ /{3} ([\s\S]+?) /{3} ([imgy]{0,4}) (?!\w) /// +REGEX = /// ^
+ (/ (?! [\s=] ) # disallow leading whitespace or equals signs
+ [^ [ / \n \\ ]* # every other thing
+ (?:
+ (?: \\[\s\S] # anything escaped
+ | \[ # character class
+ [^ \] \n \\ ]*
+ (?: \\[\s\S] [^ \] \n \\ ]* )*
+ ]
+ ) [^ [ / \n \\ ]*
+ )*
+ /) ([imgy]{0,4}) (?!\w)
+///
-HEREGEX_OMIT = /\s+(?:#.*)?/g |
Token cleaning regexes. | MULTILINER = /\n/g
+HEREGEX = /// ^ /{3} ([\s\S]+?) /{3} ([imgy]{0,4}) (?!\w) ///
-HEREDOC_INDENT = /\n+([^\n\S]*)/g
+HEREGEX_OMIT = /\s+(?:#.*)?/g MULTILINER = /\n/g
-LINE_CONTINUER = /// ^ \s* (?: , | \??\.(?![.\d]) | :: ) ///
+HEREDOC_INDENT = /\n+([^\n\S]*)/g
-TRAILING_SPACES = /\s+$/ |
Compound assignment tokens. | COMPOUND_ASSIGN = [
- '-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>=', '&=', '^=', '|='
-] |
Unary tokens. | UNARY = ['!', '~', 'NEW', 'TYPEOF', 'DELETE', 'DO'] |
Logical tokens. | LOGIC = ['&&', '||', '&', '|', '^'] |
Bit-shifting tokens. | SHIFT = ['<<', '>>', '>>>'] |
Comparison tokens. | COMPARE = ['==', '!=', '<', '>', '<=', '>='] |
Mathematical tokens. | MATH = ['*', '/', '%'] |
Relational tokens that are negatable with | RELATION = ['IN', 'OF', 'INSTANCEOF'] |
Boolean tokens. | BOOL = ['TRUE', 'FALSE'] |
Tokens which a regular expression will never immediately follow, but which -a division operator might. +HEREDOC_ILLEGAL = /\*\// -See: http://www.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions +LINE_CONTINUER = /// ^ \s* (?: , | \??\.(?![.\d]) | :: ) /// -Our list is shorter, due to sans-parentheses method calls. | NOT_REGEX = ['NUMBER', 'REGEX', 'BOOL', 'NULL', 'UNDEFINED', '++', '--', ']'] |
If the previous token is not spaced, there are more preceding tokens that -force a division parse: | NOT_SPACED_REGEX = NOT_REGEX.concat ')', '}', 'THIS', 'IDENTIFIER', 'STRING' |
Tokens which could legitimately be invoked or indexed. An opening +TRAILING_SPACES = /\s+$/ + + + + +
+
+
+
+
+ ¶
+
+ Compound assignment tokens. + + +COMPOUND_ASSIGN = [
+ '-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>=', '&=', '^=', '|='
+]
+
+
+
+
+ ¶
+
+ Unary tokens. + + +UNARY = ['!', '~', 'NEW', 'TYPEOF', 'DELETE', 'DO']
+
+
+
+
+ ¶
+
+ Logical tokens. + + +LOGIC = ['&&', '||', '&', '|', '^']
+
+
+
+
+ ¶
+
+ Bit-shifting tokens. + + +SHIFT = ['<<', '>>', '>>>']
+
+
+
+
+ ¶
+
+ Comparison tokens. + + +COMPARE = ['==', '!=', '<', '>', '<=', '>=']
+
+
+
+
+ ¶
+
+ Mathematical tokens. + + +MATH = ['*', '/', '%']
+
+
+
+
+ ¶
+
+ Relational tokens that are negatable with RELATION = ['IN', 'OF', 'INSTANCEOF']
+
+
+
+
+ ¶
+
+ Boolean tokens. + + +BOOL = ['TRUE', 'FALSE']
+
+
+
+
+ ¶
+
+ 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 + + +Our list is shorter, due to sans-parentheses method calls. + + +NOT_REGEX = ['NUMBER', 'REGEX', 'BOOL', 'NULL', 'UNDEFINED', '++', '--']
+
+
+
+
+ ¶
+
+ If the previous token is not spaced, there are more preceding tokens that +force a division parse: + + +NOT_SPACED_REGEX = NOT_REGEX.concat ')', '}', 'THIS', 'IDENTIFIER', 'STRING', ']'
+
+
+ ¶
+
+ Tokens which could legitimately be invoked or indexed. An opening parentheses or bracket following these tokens will be recorded as the start -of a function invocation or indexing operation. | CALLABLE = ['IDENTIFIER', 'STRING', 'REGEX', ')', ']', '}', '?', '::', '@', 'THIS', 'SUPER']
-INDEXABLE = CALLABLE.concat 'NUMBER', 'BOOL', 'NULL', 'UNDEFINED' |
Tokens that, when immediately preceding a CALLABLE = ['IDENTIFIER', 'STRING', 'REGEX', ')', ']', '}', '?', '::', '@', 'THIS', 'SUPER']
+INDEXABLE = CALLABLE.concat 'NUMBER', 'BOOL', 'NULL', 'UNDEFINED'
+
+
+ ¶
+
+ Tokens that, when immediately preceding a | LINE_BREAK = ['INDENT', 'OUTDENT', 'TERMINATOR']
+avoid an ambiguity in the grammar.
+
- |
LINE_BREAK = ['INDENT', 'OUTDENT', 'TERMINATOR']
nodes.coffee | |
---|---|
+
+
+
| Error.stackTraceLimit = Infinity
-
-{Scope} = require './scope'
-{RESERVED, STRICT_PROSCRIBED} = require './lexer' |
Import the helpers we plan to use. | {compact, flatten, extend, merge, del, starts, ends, last, some, addLocationDataFn, locationDataToString} = require './helpers' |
Functions required by parser | exports.extend = extend
-exports.addLocationDataFn = addLocationDataFn |
Constant functions for nodes that don't need customization. | YES = -> yes
-NO = -> no
-THIS = -> this
-NEGATE = -> @negated = not @negated; this |
CodeFragment | |
The various nodes defined below all compile to a collection of CodeFragment objects.
+the syntax tree into a string of JavaScript code, call
+Error.stackTraceLimit = Infinity
+
+{Scope} = require './scope'
+{RESERVED, STRICT_PROSCRIBED} = require './lexer'
+
+
+
+
+ ¶
+
+ Import the helpers we plan to use. + + +{compact, flatten, extend, merge, del, starts, ends, last, some,
+addLocationDataFn, locationDataToString, throwSyntaxError} = require './helpers'
+
+
+
+
+ ¶
+
+ Functions required by parser + + +exports.extend = extend
+exports.addLocationDataFn = addLocationDataFn
+
+
+
+
+ ¶
+
+ Constant functions for nodes that don't need customization. + + +YES = -> yes
+NO = -> no
+THIS = -> this
+NEGATE = -> @negated = not @negated; this
+
+
+
+
+ ¶
+
+ CodeFragment+ +
+
+
+ ¶
+
+ The various nodes defined below all compile to a collection of CodeFragment objects.
A CodeFragments is a block of generated code, and the location in the source file where the code
came from. CodeFragments can be assembled together into working code just by catting together
-all the CodeFragments' | exports.CodeFragment = class CodeFragment
- constructor: (parent, code) ->
- @code = "#{code}"
- @locationData = parent?.locationData
- @type = parent?.constructor?.name or 'unknown'
-
- toString: () ->
- "#{@code}#{[if @locationData then ": " + locationDataToString(@locationData)]}" |
Convert an array of CodeFragments into a string. | fragmentsToText = (fragments) ->
- (fragment.code for fragment in fragments).join('') |
Base | |
The Base is the abstract base class for all nodes in the syntax tree.
+all the CodeFragments' exports.CodeFragment = class CodeFragment
+ constructor: (parent, code) ->
+ @code = "#{code}"
+ @locationData = parent?.locationData
+ @type = parent?.constructor?.name or 'unknown'
+
+ toString: ->
+ "#{@code}#{if @locationData then ": " + locationDataToString(@locationData) else ''}"
+
+
+
+
+ ¶
+
+ Convert an array of CodeFragments into a string. + + +fragmentsToText = (fragments) ->
+ (fragment.code for fragment in fragments).join('')
+
+
+
+
+ ¶
+
+ Base+ +
+
+
+ ¶
+
+ The Base is the abstract base class for all nodes in the syntax tree.
Each subclass implements the | exports.Base = class Base
-
- compile: (o, lvl) ->
- fragmentsToText @compileToFragments o, lvl |
Common logic for determining whether to wrap this node in a closure before +scope, and indentation level. + + + + +exports.Base = class Base
+
+ compile: (o, lvl) ->
+ fragmentsToText @compileToFragments o, lvl
+
+
+ ¶
+
+ Common logic for determining whether to wrap this node in a closure before compiling it, or to compile directly. We need to wrap if this node is a -statement, and it's not a pureStatement, and we're not at -the top level of a block (which would be unnecessary), and we haven't +statement, and it's not a pureStatement, and we're not at +the top level of a block (which would be unnecessary), and we haven't already been asked to return the result (because statements know how to -return results). | compileToFragments: (o, lvl) ->
- o = extend {}, o
- o.level = lvl if lvl
- node = @unfoldSoak(o) or this
- node.tab = o.indent
- if o.level is LEVEL_TOP or not node.isStatement(o)
- node.compileNode o
- else
- node.compileClosure o |
Statements converted into expressions via closure-wrapping share a scope -object with their parent closure, to preserve the expected lexical scope. | compileClosure: (o) ->
- if @jumps()
- throw SyntaxError 'cannot use a pure statement in an expression.'
- o.sharedScope = yes
- Closure.wrap(this).compileNode o |
If the code generation wishes to use the result of a complex expression +return results). + + + + + compileToFragments: (o, lvl) ->
+ o = extend {}, o
+ o.level = lvl if lvl
+ node = @unfoldSoak(o) or this
+ node.tab = o.indent
+ if o.level is LEVEL_TOP or not node.isStatement(o)
+ node.compileNode o
+ else
+ node.compileClosure o
+
+
+
+
+ ¶
+
+ Statements converted into expressions via closure-wrapping share a scope +object with their parent closure, to preserve the expected lexical scope. + + + compileClosure: (o) ->
+ if jumpNode = @jumps()
+ jumpNode.error 'cannot use a pure statement in an expression'
+ o.sharedScope = yes
+ Closure.wrap(this).compileNode o
+
+
+ ¶
+
+ If the code generation wishes to use the result of a complex expression in multiple places, ensure that the expression is only ever evaluated once, -by assigning it to a temporary variable. Pass a level to precompile. +by assigning it to a temporary variable. Pass a level to precompile. +If | cache: (o, level, reused) ->
- unless @isComplex()
- ref = if level then @compileToFragments o, level else this
- [ref, ref]
- else
- ref = new Literal reused or o.scope.freeVariable 'ref'
- sub = new Assign ref, this
- if level then [sub.compileToFragments(o, level), [@makeCode(ref.value)]] else [sub, ref]
-
- cacheToCodeFragments: (cacheValues) ->
- [fragmentsToText(cacheValues[0]), fragmentsToText(cacheValues[1])] |
Construct a node that returns the current node's result. +the two values are raw nodes which have not been compiled. + + + + + cache: (o, level, reused) ->
+ unless @isComplex()
+ ref = if level then @compileToFragments o, level else this
+ [ref, ref]
+ else
+ ref = new Literal reused or o.scope.freeVariable 'ref'
+ sub = new Assign ref, this
+ if level then [sub.compileToFragments(o, level), [@makeCode(ref.value)]] else [sub, ref]
+
+ cacheToCodeFragments: (cacheValues) ->
+ [fragmentsToText(cacheValues[0]), fragmentsToText(cacheValues[1])]
+
+
+ ¶
+
+ Construct a node that returns the current node's result. Note that this is overridden for smarter behavior for -many statement nodes (e.g. If, For)... | makeReturn: (res) ->
- me = @unwrapAll()
- if res
- new Call new Literal("#{res}.push"), [me]
- else
- new Return me |
Does this node, or any of its children, contain a node of a certain kind?
-Recursively traverses down the children of the nodes, yielding to a block
-and returning true when the block finds a match. | contains: (pred) ->
- contains = no
- @traverseChildren no, (node) ->
- if pred node
- contains = yes
- return no
- contains |
Is this node of a certain type, or does it contain the type? | containsType: (type) ->
- this instanceof type or @contains (node) -> node instanceof type |
Pull out the last non-comment node of a node list. | lastNonComment: (list) ->
- i = list.length
- return list[i] while i-- when list[i] not instanceof Comment
- null |
| toString: (idt = '', name = @constructor.name) ->
- location = if @locationData then locationDataToString @locationData else "??"
- tree = '\n' + idt + location + ": " + name
- tree += '?' if @soak
- @eachChild (node) -> tree += node.toString idt + TAB
- tree |
Passes each child to a function, breaking when the function returns | eachChild: (func) ->
- return this unless @children
- for attr in @children when @[attr]
- for child in flatten [@[attr]]
- return this if func(child) is false
- this
-
- traverseChildren: (crossScope, func) ->
- @eachChild (child) ->
- return false if func(child) is false
- child.traverseChildren crossScope, func
-
- invert: ->
- new Op '!', this
-
- unwrapAll: ->
- node = this
- continue until node is node = node.unwrap()
- node |
Default implementations of the common node properties and methods. Nodes -will override these with custom logic, if needed. | children: []
-
- isStatement : NO
- jumps : NO
- isComplex : YES
- isChainable : NO
- isAssignable : NO
-
- unwrap : THIS
- unfoldSoak : NO |
Is this node used to assign a certain variable? | assigns: NO |
For this node and all descendents, set the location data to | updateLocationDataIfMissing: (locationData) ->
- if not @locationData
- @locationData = {}
- extend @locationData, locationData
-
- @eachChild (child) ->
- child.updateLocationDataIfMissing locationData
-
- makeCode: (code) ->
- new CodeFragment this, code
-
- wrapInBraces: (fragments) ->
- [].concat @makeCode('('), fragments, @makeCode(')') |
makeReturn: (res) ->
+ me = @unwrapAll()
+ if res
+ new Call new Literal("#{res}.push"), [me]
+ else
+ new Return me
+
+
+
+
+ ¶
+
+ Does this node, or any of its children, contain a node of a certain kind?
+Recursively traverses down the children nodes and returns the first one
+that verifies contains: (pred) ->
+ node = undefined
+ @traverseChildren no, (n) ->
+ if pred n
+ node = n
+ return no
+ node
+
+
+
+
+ ¶
+
+ Pull out the last non-comment node of a node list. + + + lastNonComment: (list) ->
+ i = list.length
+ return list[i] while i-- when list[i] not instanceof Comment
+ null
+
+
+
+
+ ¶
+
+
toString: (idt = '', name = @constructor.name) ->
+ tree = '\n' + idt + name
+ tree += '?' if @soak
+ @eachChild (node) -> tree += node.toString idt + TAB
+ tree
+
+
+
+
+ ¶
+
+ Passes each child to a function, breaking when the function returns eachChild: (func) ->
+ return this unless @children
+ for attr in @children when @[attr]
+ for child in flatten [@[attr]]
+ return this if func(child) is false
+ this
+
+ traverseChildren: (crossScope, func) ->
+ @eachChild (child) ->
+ recur = func(child)
+ child.traverseChildren(crossScope, func) unless recur is no
+
+ invert: ->
+ new Op '!', this
+
+ unwrapAll: ->
+ node = this
+ continue until node is node = node.unwrap()
+ node
+
+
+
+
+ ¶
+
+ Default implementations of the common node properties and methods. Nodes +will override these with custom logic, if needed. + + + children: []
+
+ isStatement : NO
+ jumps : NO
+ isComplex : YES
+ isChainable : NO
+ isAssignable : NO
+
+ unwrap : THIS
+ unfoldSoak : NO
+
+
+
+
+ ¶
+
+ Is this node used to assign a certain variable? + + + assigns: NO
+
+
+
+
+ ¶
+
+ For this node and all descendents, set the location data to updateLocationDataIfMissing: (locationData) ->
+ @locationData or= locationData
+
+ @eachChild (child) ->
+ child.updateLocationDataIfMissing locationData
+
+
+
+
+ ¶
+
+ Throw a SyntaxError associated with this node's location. + + + error: (message) ->
+ throwSyntaxError message, @locationData
+
+ makeCode: (code) ->
+ new CodeFragment this, code
+
+ wrapInBraces: (fragments) ->
+ [].concat @makeCode('('), fragments, @makeCode(')')
+
+
+ ¶
+
+
| joinFragmentArrays: (fragmentsList, joinStr) ->
- answer = []
- for fragments,i in fragmentsList
- if i then answer.push @makeCode joinStr
- answer = answer.concat fragments
- answer |
Block | |
The block is the list of expressions that forms the body of an +of fragments. + + + + + joinFragmentArrays: (fragmentsList, joinStr) ->
+ answer = []
+ for fragments,i in fragmentsList
+ if i then answer.push @makeCode joinStr
+ answer = answer.concat fragments
+ answer
+
+
+
+
+ ¶
+
+ Block+ +
+
+
+ ¶
+
+ The block is the list of expressions that forms the body of an
indented block of code -- the implementation of a function, a clause in an
- | exports.Block = class Block extends Base
- constructor: (nodes) ->
- @expressions = compact flatten nodes or []
-
- children: ['expressions'] |
Tack an expression on to the end of this expression list. | push: (node) ->
- @expressions.push node
- this |
Remove and return the last expression of this expression list. | pop: ->
- @expressions.pop() |
Add an expression at the beginning of this expression list. | unshift: (node) ->
- @expressions.unshift node
- this |
If this Block consists of just a single node, unwrap it by pulling -it back out. | unwrap: ->
- if @expressions.length is 1 then @expressions[0] else this |
Is this an empty block of code? | isEmpty: ->
- not @expressions.length
-
- isStatement: (o) ->
- for exp in @expressions when exp.isStatement o
- return yes
- no
-
- jumps: (o) ->
- for exp in @expressions
- return exp if exp.jumps o |
A Block node does not return its entire body, rather it -ensures that the final expression is returned. | makeReturn: (res) ->
- len = @expressions.length
- while len--
- expr = @expressions[len]
- if expr not instanceof Comment
- @expressions[len] = expr.makeReturn res
- @expressions.splice(len, 1) if expr instanceof Return and not expr.expression
- break
- this |
A Block is the only node that can serve as the root. | compileToFragments: (o = {}, level) ->
- if o.scope then super o, level else @compileRoot o |
Compile all expressions within the Block body. If we need to -return the result, and it's an expression, simply return it. If it's a -statement, ask the statement to do so. | compileNode: (o) ->
- @tab = o.indent
- top = o.level is LEVEL_TOP
- compiledNodes = []
-
- for node, index in @expressions
-
- node = node.unwrapAll()
- node = (node.unfoldSoak(o) or node)
- if node instanceof Block |
This is a nested block. We don't do anything special here like enclose
+ exports.Block = class Block extends Base
+ constructor: (nodes) ->
+ @expressions = compact flatten nodes or []
+
+ children: ['expressions']
+
+
+
+
+ ¶
+
+ Tack an expression on to the end of this expression list. + + + push: (node) ->
+ @expressions.push node
+ this
+
+
+
+
+ ¶
+
+ Remove and return the last expression of this expression list. + + + pop: ->
+ @expressions.pop()
+
+
+
+
+ ¶
+
+ Add an expression at the beginning of this expression list. + + + unshift: (node) ->
+ @expressions.unshift node
+ this
+
+
+
+
+ ¶
+
+ If this Block consists of just a single node, unwrap it by pulling +it back out. + + + unwrap: ->
+ if @expressions.length is 1 then @expressions[0] else this
+
+
+
+
+ ¶
+
+ Is this an empty block of code? + + + isEmpty: ->
+ not @expressions.length
+
+ isStatement: (o) ->
+ for exp in @expressions when exp.isStatement o
+ return yes
+ no
+
+ jumps: (o) ->
+ for exp in @expressions
+ return exp if exp.jumps o
+
+
+
+
+ ¶
+
+ A Block node does not return its entire body, rather it +ensures that the final expression is returned. + + + makeReturn: (res) ->
+ len = @expressions.length
+ while len--
+ expr = @expressions[len]
+ if expr not instanceof Comment
+ @expressions[len] = expr.makeReturn res
+ @expressions.splice(len, 1) if expr instanceof Return and not expr.expression
+ break
+ this
+
+
+
+
+ ¶
+
+ A Block is the only node that can serve as the root. + + + compileToFragments: (o = {}, level) ->
+ if o.scope then super o, level else @compileRoot o
+
+
+
+
+ ¶
+
+ Compile all expressions within the Block body. If we need to +return the result, and it's an expression, simply return it. If it's a +statement, ask the statement to do so. + + + compileNode: (o) ->
+ @tab = o.indent
+ top = o.level is LEVEL_TOP
+ compiledNodes = []
+
+ for node, index in @expressions
+
+ node = node.unwrapAll()
+ node = (node.unfoldSoak(o) or node)
+ if node instanceof Block
+
+
+ ¶
+
+ This is a nested block. We don't do anything special here like enclose it in a new scope; we just compile the statements in this block along with -our own | compiledNodes.push node.compileNode o
- else if top
- node.front = true
- fragments = node.compileToFragments o
- unless node.isStatement o
- fragments.unshift @makeCode "#{@tab}"
- fragments.push @makeCode ";"
- compiledNodes.push fragments
- else
- compiledNodes.push node.compileToFragments o, LEVEL_LIST
- if top
- if @spaced
- return [].concat @makeCode("\n"), @joinFragmentArrays(compiledNodes, '\n\n'), @makeCode("\n")
- else
- return @joinFragmentArrays(compiledNodes, '\n')
- if compiledNodes.length
- answer = @joinFragmentArrays(compiledNodes, ', ')
- else
- answer = [@makeCode "void 0"]
- if compiledNodes.length > 1 and o.level >= LEVEL_LIST then @wrapInBraces answer else answer |
If we happen to be the top-level Block, wrap everything in +our own + + + + + compiledNodes.push node.compileNode o
+ else if top
+ node.front = true
+ fragments = node.compileToFragments o
+ unless node.isStatement o
+ fragments.unshift @makeCode "#{@tab}"
+ fragments.push @makeCode ";"
+ compiledNodes.push fragments
+ else
+ compiledNodes.push node.compileToFragments o, LEVEL_LIST
+ if top
+ if @spaced
+ return [].concat @joinFragmentArrays(compiledNodes, '\n\n'), @makeCode("\n")
+ else
+ return @joinFragmentArrays(compiledNodes, '\n')
+ if compiledNodes.length
+ answer = @joinFragmentArrays(compiledNodes, ', ')
+ else
+ answer = [@makeCode "void 0"]
+ if compiledNodes.length > 1 and o.level >= LEVEL_LIST then @wrapInBraces answer else answer
+
+
+ ¶
+
+ If we happen to be the top-level Block, wrap everything in a safety closure, unless requested not to. It would be better not to generate them in the first place, but for now, -clean up obvious double-parentheses. | compileRoot: (o) ->
- o.indent = if o.bare then '' else TAB
- o.scope = new Scope null, this, null
- o.level = LEVEL_TOP
- @spaced = yes
- prelude = []
- unless o.bare
- preludeExps = for exp, i in @expressions
- break unless exp.unwrap() instanceof Comment
- exp
- rest = @expressions[preludeExps.length...]
- @expressions = preludeExps
- if preludeExps.length
- prelude = @compileNode merge(o, indent: '')
- prelude.push @makeCode "\n"
- @expressions = rest
- fragments = @compileWithDeclarations o
- return fragments if o.bare
- [].concat prelude, @makeCode("(function() {\n"), fragments, @makeCode("\n}).call(this);\n") |
Compile the expressions body for the contents of a function, with -declarations of all inner variables pushed up to the top. | compileWithDeclarations: (o) ->
- fragments = []
- post = []
- for exp, i in @expressions
- exp = exp.unwrap()
- break unless exp instanceof Comment or exp instanceof Literal
- o = merge(o, level: LEVEL_TOP)
- if i
- rest = @expressions.splice i, 9e9
- [spaced, @spaced] = [@spaced, no]
- [fragments, @spaced] = [(@compileNode o), spaced]
- @expressions = rest
- post = @compileNode o
- {scope} = o
- if scope.expressions is this
- declars = o.scope.hasDeclarations()
- assigns = scope.hasAssignments
- if declars or assigns
- fragments.push @makeCode '\n' if i
- fragments.push @makeCode "#{@tab}var "
- if declars
- fragments.push @makeCode(scope.declaredVariables().join ', ')
- if assigns
- fragments.push @makeCode ",\n#{@tab + TAB}" if declars
- fragments.push @makeCode (scope.assignedVariables().join ",\n#{@tab + TAB}")
- fragments.push @makeCode ';\n'
- fragments.concat post |
Wrap up the given nodes as a Block, unless it already happens -to be one. | @wrap: (nodes) ->
- return nodes[0] if nodes.length is 1 and nodes[0] instanceof Block
- new Block nodes |
Literal | |
Literals are static values that can be passed through directly into +clean up obvious double-parentheses. + + + + + compileRoot: (o) ->
+ o.indent = if o.bare then '' else TAB
+ o.level = LEVEL_TOP
+ @spaced = yes
+ o.scope = new Scope null, this, null
+
+
+
+
+ ¶
+
+ Mark given local variables in the root scope as parameters so they don't +end up being declared on this block. + + + o.scope.parameter name for name in o.locals or []
+ prelude = []
+ unless o.bare
+ preludeExps = for exp, i in @expressions
+ break unless exp.unwrap() instanceof Comment
+ exp
+ rest = @expressions[preludeExps.length...]
+ @expressions = preludeExps
+ if preludeExps.length
+ prelude = @compileNode merge(o, indent: '')
+ prelude.push @makeCode "\n"
+ @expressions = rest
+ fragments = @compileWithDeclarations o
+ return fragments if o.bare
+ [].concat prelude, @makeCode("(function() {\n"), fragments, @makeCode("\n}).call(this);\n")
+
+
+
+
+ ¶
+
+ Compile the expressions body for the contents of a function, with +declarations of all inner variables pushed up to the top. + + + compileWithDeclarations: (o) ->
+ fragments = []
+ post = []
+ for exp, i in @expressions
+ exp = exp.unwrap()
+ break unless exp instanceof Comment or exp instanceof Literal
+ o = merge(o, level: LEVEL_TOP)
+ if i
+ rest = @expressions.splice i, 9e9
+ [spaced, @spaced] = [@spaced, no]
+ [fragments, @spaced] = [@compileNode(o), spaced]
+ @expressions = rest
+ post = @compileNode o
+ {scope} = o
+ if scope.expressions is this
+ declars = o.scope.hasDeclarations()
+ assigns = scope.hasAssignments
+ if declars or assigns
+ fragments.push @makeCode '\n' if i
+ fragments.push @makeCode "#{@tab}var "
+ if declars
+ fragments.push @makeCode scope.declaredVariables().join(', ')
+ if assigns
+ fragments.push @makeCode ",\n#{@tab + TAB}" if declars
+ fragments.push @makeCode scope.assignedVariables().join(",\n#{@tab + TAB}")
+ fragments.push @makeCode ";\n#{if @spaced then '\n' else ''}"
+ else if fragments.length and post.length
+ fragments.push @makeCode "\n"
+ fragments.concat post
+
+
+
+
+ ¶
+
+ Wrap up the given nodes as a Block, unless it already happens +to be one. + + + @wrap: (nodes) ->
+ return nodes[0] if nodes.length is 1 and nodes[0] instanceof Block
+ new Block nodes
+
+
+
+
+ ¶
+
+ Literal+ +
+
+
+ ¶
+
+ Literals are static values that can be passed through directly into
JavaScript without translation, such as: strings, numbers,
- | exports.Literal = class Literal extends Base
- constructor: (@value) ->
-
- makeReturn: ->
- if @isStatement() then this else super
-
- isAssignable: ->
- IDENTIFIER.test @value
-
- isStatement: ->
- @value in ['break', 'continue', 'debugger']
-
- isComplex: NO
-
- assigns: (name) ->
- name is @value
-
- jumps: (o) ->
- return this if @value is 'break' and not (o?.loop or o?.block)
- return this if @value is 'continue' and not o?.loop
-
- compileNode: (o) ->
- code = if @value is 'this'
- if o.scope.method?.bound then o.scope.method.context else @value
- else if @value.reserved
- "\"#{@value}\""
- else
- @value
- answer = if @isStatement() then "#{@tab}#{code};" else code
- [@makeCode answer]
-
- toString: ->
- ' "' + @value + '"'
-
-class exports.Undefined extends Base
- isAssignable: NO
- isComplex: NO
- compileNode: (o) ->
- [@makeCode if o.level >= LEVEL_ACCESS then '(void 0)' else 'void 0']
-
-class exports.Null extends Base
- isAssignable: NO
- isComplex: NO
- compileNode: -> [@makeCode "null"]
-
-class exports.Bool extends Base
- isAssignable: NO
- isComplex: NO
- compileNode: -> [@makeCode @val]
- constructor: (@val) -> |
Return | |
A | exports.Return = class Return extends Base
- constructor: (expr) ->
- @expression = expr if expr and not expr.unwrap().isUndefined
-
- children: ['expression']
-
- isStatement: YES
- makeReturn: THIS
- jumps: THIS
-
- compileToFragments: (o, level) ->
- expr = @expression?.makeReturn()
- if expr and expr not instanceof Return then expr.compileToFragments o, level else super o, level
-
- compileNode: (o) ->
- answer = [] |
TODO: If we call expression.compile() here twice, we'll sometimes get back different results! | answer.push @makeCode(@tab + "return#{[" " if @expression]}")
- if @expression
- answer = answer.concat @expression.compileToFragments(o, LEVEL_PAREN)
- answer.push @makeCode ";"
- return answer |
Value | |
A value, variable or literal or parenthesized, indexed or dotted into, -or vanilla. | exports.Value = class Value extends Base
- constructor: (base, props, tag) ->
- return base if not props and base instanceof Value
- @base = base
- @properties = props or []
- @[tag] = true if tag
- return this
-
- children: ['base', 'properties'] |
Add a property (or properties ) | add: (props) ->
- @properties = @properties.concat props
- this
-
- hasProperties: ->
- !!@properties.length |
Some boolean checks for the benefit of other nodes. | isArray : -> not @properties.length and @base instanceof Arr
- isComplex : -> @hasProperties() or @base.isComplex()
- isAssignable : -> @hasProperties() or @base.isAssignable()
- isSimpleNumber : -> @base instanceof Literal and SIMPLENUM.test @base.value
- isString : -> @base instanceof Literal and IS_STRING.test @base.value
- isAtomic : ->
- for node in @properties.concat @base
- return no if node.soak or node instanceof Call
- yes
-
- isStatement : (o) -> not @properties.length and @base.isStatement o
- assigns : (name) -> not @properties.length and @base.assigns name
- jumps : (o) -> not @properties.length and @base.jumps o
-
- isObject: (onlyGenerated) ->
- return no if @properties.length
- (@base instanceof Obj) and (not onlyGenerated or @base.generated)
-
- isSplice: ->
- last(@properties) instanceof Slice |
The value can be unwrapped as its inner node, if there are no attached -properties. | unwrap: ->
- if @properties.length then this else @base |
A reference has base part ( exports.Literal = class Literal extends Base
+ constructor: (@value) ->
+
+ makeReturn: ->
+ if @isStatement() then this else super
+
+ isAssignable: ->
+ IDENTIFIER.test @value
+
+ isStatement: ->
+ @value in ['break', 'continue', 'debugger']
+
+ isComplex: NO
+
+ assigns: (name) ->
+ name is @value
+
+ jumps: (o) ->
+ return this if @value is 'break' and not (o?.loop or o?.block)
+ return this if @value is 'continue' and not o?.loop
+
+ compileNode: (o) ->
+ code = if @value is 'this'
+ if o.scope.method?.bound then o.scope.method.context else @value
+ else if @value.reserved
+ "\"#{@value}\""
+ else
+ @value
+ answer = if @isStatement() then "#{@tab}#{code};" else code
+ [@makeCode answer]
+
+ toString: ->
+ ' "' + @value + '"'
+
+class exports.Undefined extends Base
+ isAssignable: NO
+ isComplex: NO
+ compileNode: (o) ->
+ [@makeCode if o.level >= LEVEL_ACCESS then '(void 0)' else 'void 0']
+
+class exports.Null extends Base
+ isAssignable: NO
+ isComplex: NO
+ compileNode: -> [@makeCode "null"]
+
+class exports.Bool extends Base
+ isAssignable: NO
+ isComplex: NO
+ compileNode: -> [@makeCode @val]
+ constructor: (@val) ->
+
+
+
+
+ ¶
+
+ Return+ +
+
+
+
+
+ ¶
+
+ A exports.Return = class Return extends Base
+ constructor: (expr) ->
+ @expression = expr if expr and not expr.unwrap().isUndefined
+
+ children: ['expression']
+
+ isStatement: YES
+ makeReturn: THIS
+ jumps: THIS
+
+ compileToFragments: (o, level) ->
+ expr = @expression?.makeReturn()
+ if expr and expr not instanceof Return then expr.compileToFragments o, level else super o, level
+
+ compileNode: (o) ->
+ answer = []
+
+
+
+
+ ¶
+
+ TODO: If we call expression.compile() here twice, we'll sometimes get back different results! + + + answer.push @makeCode @tab + "return#{if @expression then " " else ""}"
+ if @expression
+ answer = answer.concat @expression.compileToFragments o, LEVEL_PAREN
+ answer.push @makeCode ";"
+ return answer
+
+
+
+
+ ¶
+
+ Value+ +
+
+
+
+
+ ¶
+
+ A value, variable or literal or parenthesized, indexed or dotted into, +or vanilla. + + +exports.Value = class Value extends Base
+ constructor: (base, props, tag) ->
+ return base if not props and base instanceof Value
+ @base = base
+ @properties = props or []
+ @[tag] = true if tag
+ return this
+
+ children: ['base', 'properties']
+
+
+
+
+ ¶
+
+ Add a property (or properties ) add: (props) ->
+ @properties = @properties.concat props
+ this
+
+ hasProperties: ->
+ !!@properties.length
+
+
+
+
+ ¶
+
+ Some boolean checks for the benefit of other nodes. + + + isArray : -> not @properties.length and @base instanceof Arr
+ isComplex : -> @hasProperties() or @base.isComplex()
+ isAssignable : -> @hasProperties() or @base.isAssignable()
+ isSimpleNumber : -> @base instanceof Literal and SIMPLENUM.test @base.value
+ isString : -> @base instanceof Literal and IS_STRING.test @base.value
+ isAtomic : ->
+ for node in @properties.concat @base
+ return no if node.soak or node instanceof Call
+ yes
+
+ isStatement : (o) -> not @properties.length and @base.isStatement o
+ assigns : (name) -> not @properties.length and @base.assigns name
+ jumps : (o) -> not @properties.length and @base.jumps o
+
+ isObject: (onlyGenerated) ->
+ return no if @properties.length
+ (@base instanceof Obj) and (not onlyGenerated or @base.generated)
+
+ isSplice: ->
+ last(@properties) instanceof Slice
+
+
+
+
+ ¶
+
+ The value can be unwrapped as its inner node, if there are no attached +properties. + + + unwrap: ->
+ if @properties.length then this else @base
+
+
+ ¶
+
+ A reference has base part ( | cacheReference: (o) ->
- name = last @properties
- if @properties.length < 2 and not @base.isComplex() and not name?.isComplex()
- return [this, this] # `a` `a.b`
- base = new Value @base, @properties[...-1]
- if base.isComplex() # `a().b`
- bref = new Literal o.scope.freeVariable 'base'
- base = new Value new Parens new Assign bref, base
- return [base, bref] unless name # `a()`
- if name.isComplex() # `a[b()]`
- nref = new Literal o.scope.freeVariable 'name'
- name = new Index new Assign nref, name.index
- nref = new Index nref
- [base.add(name), new Value(bref or base.base, [nref or name])] |
We compile a value to JavaScript by compiling and joining each property.
+ cacheReference: (o) ->
+ name = last @properties
+ if @properties.length < 2 and not @base.isComplex() and not name?.isComplex()
+ return [this, this] # `a` `a.b`
+ base = new Value @base, @properties[...-1]
+ if base.isComplex() # `a().b`
+ bref = new Literal o.scope.freeVariable 'base'
+ base = new Value new Parens new Assign bref, base
+ return [base, bref] unless name # `a()`
+ if name.isComplex() # `a[b()]`
+ nref = new Literal o.scope.freeVariable 'name'
+ name = new Index new Assign nref, name.index
+ nref = new Index nref
+ [base.add(name), new Value(bref or base.base, [nref or name])]
+
+
+ ¶
+
+ We compile a value to JavaScript by compiling and joining each property.
Things get much more interesting if the chain of properties has soak
operators | compileNode: (o) ->
- @base.front = @front
- props = @properties
- fragments = @base.compileToFragments o, (if props.length then LEVEL_ACCESS else null)
- if (@base instanceof Parens or props.length) and SIMPLENUM.test fragmentsToText fragments
- fragments.push @makeCode '.'
- for prop in props
- fragments.push (prop.compileToFragments o)...
- fragments |
Unfold a soak into an | unfoldSoak: (o) ->
- @unfoldedSoak ?= do =>
- if ifn = @base.unfoldSoak o
- ifn.body.properties.push @properties...
- return ifn
- for prop, i in @properties when prop.soak
- prop.soak = off
- fst = new Value @base, @properties[...i]
- snd = new Value @base, @properties[i..]
- if fst.isComplex()
- ref = new Literal o.scope.freeVariable 'ref'
- fst = new Parens new Assign ref, fst
- snd.base = ref
- return new If new Existence(fst), snd, soak: on
- no |
Comment | |
CoffeeScript passes through block comments as JavaScript block comments -at the same position. | exports.Comment = class Comment extends Base
- constructor: (@comment) ->
-
- isStatement: YES
- makeReturn: THIS
-
- compileNode: (o, level) ->
- code = '/*' + multident(@comment, @tab) + "\n#{@tab}*/\n"
- code = o.indent + code if (level or o.level) is LEVEL_TOP
- [@makeCode code] |
Call | |
Node for a function invocation. Takes care of converting | exports.Call = class Call extends Base
- constructor: (variable, @args = [], @soak) ->
- @isNew = false
- @isSuper = variable is 'super'
- @variable = if @isSuper then null else variable
-
- children: ['variable', 'args'] |
Tag this invocation as creating a new instance. | newInstance: ->
- base = @variable?.base or @variable
- if base instanceof Call and not base.isNew
- base.newInstance()
- else
- @isNew = true
- this |
Grab the reference to the superclass's implementation of the current -method. | superReference: (o) ->
- method = o.scope.namedMethod()
- if method?.klass
- accesses = [new Access(new Literal '__super__')]
- accesses.push new Access new Literal 'constructor' if method.static
- accesses.push new Access new Literal method.name
- (new Value (new Literal method.klass), accesses).compile o
- else if method?.ctor
- "#{method.name}.__super__.constructor"
- else
- throw SyntaxError 'cannot call super outside of an instance method.' |
The appropriate | superThis : (o) ->
- method = o.scope.method
- (method and not method.klass and method.context) or "this" |
Soaked chained invocations unfold into if/else ternary structures. | unfoldSoak: (o) ->
- if @soak
- if @variable
- return ifn if ifn = unfoldSoak o, this, 'variable'
- [left, rite] = new Value(@variable).cacheReference o
- else
- left = new Literal @superReference o
- rite = new Value left
- rite = new Call rite, @args
- rite.isNew = @isNew
- left = new Literal "typeof #{ left.compile o } === \"function\""
- return new If left, new Value(rite), soak: yes
- call = this
- list = []
- loop
- if call.variable instanceof Call
- list.push call
- call = call.variable
- continue
- break unless call.variable instanceof Value
- list.push call
- break unless (call = call.variable.base) instanceof Call
- for call in list.reverse()
- if ifn
- if call.variable instanceof Call
- call.variable = ifn
- else
- call.variable.base = ifn
- ifn = unfoldSoak o, call, 'variable'
- ifn |
Compile a vanilla function call. | compileNode: (o) ->
- @variable?.front = @front
- compiledArray = Splat.compileSplattedArray o, @args, true
- if compiledArray.length
- return @compileSplat o, compiledArray
- compiledArgs = []
- for arg, argIndex in @args
- if argIndex then compiledArgs.push @makeCode ", "
- compiledArgs.push (arg.compileToFragments o, LEVEL_LIST)...
-
- fragments = []
- if @isSuper
- preface = @superReference(o) + ".call(#{@superThis(o)}"
- if compiledArgs.length then preface += ", "
- fragments.push @makeCode preface
- else
- if @isNew then fragments.push @makeCode 'new '
- fragments.push (@variable.compileToFragments(o, LEVEL_ACCESS))...
- fragments.push @makeCode "("
- fragments.push compiledArgs...
- fragments.push @makeCode ")"
- fragments |
If you call a function with a splat, it's converted into a JavaScript +evaluate anything twice when building the soak chain. + + + + + compileNode: (o) ->
+ @base.front = @front
+ props = @properties
+ fragments = @base.compileToFragments o, (if props.length then LEVEL_ACCESS else null)
+ if (@base instanceof Parens or props.length) and SIMPLENUM.test fragmentsToText fragments
+ fragments.push @makeCode '.'
+ for prop in props
+ fragments.push (prop.compileToFragments o)...
+ fragments
+
+
+
+
+ ¶
+
+ Unfold a soak into an unfoldSoak: (o) ->
+ @unfoldedSoak ?= do =>
+ if ifn = @base.unfoldSoak o
+ ifn.body.properties.push @properties...
+ return ifn
+ for prop, i in @properties when prop.soak
+ prop.soak = off
+ fst = new Value @base, @properties[...i]
+ snd = new Value @base, @properties[i..]
+ if fst.isComplex()
+ ref = new Literal o.scope.freeVariable 'ref'
+ fst = new Parens new Assign ref, fst
+ snd.base = ref
+ return new If new Existence(fst), snd, soak: on
+ no
+
+
+
+
+ ¶
+
+ Comment+ +
+
+
+
+
+ ¶
+
+ CoffeeScript passes through block comments as JavaScript block comments +at the same position. + + +exports.Comment = class Comment extends Base
+ constructor: (@comment) ->
+
+ isStatement: YES
+ makeReturn: THIS
+
+ compileNode: (o, level) ->
+ code = "/*#{multident @comment, @tab}#{if '\n' in @comment then "\n#{@tab}" else ''}*/\n"
+ code = o.indent + code if (level or o.level) is LEVEL_TOP
+ [@makeCode code]
+
+
+
+
+ ¶
+
+ Call+ +
+
+
+
+
+ ¶
+
+ Node for a function invocation. Takes care of converting exports.Call = class Call extends Base
+ constructor: (variable, @args = [], @soak) ->
+ @isNew = false
+ @isSuper = variable is 'super'
+ @variable = if @isSuper then null else variable
+
+ children: ['variable', 'args']
+
+
+
+
+ ¶
+
+ Tag this invocation as creating a new instance. + + + newInstance: ->
+ base = @variable?.base or @variable
+ if base instanceof Call and not base.isNew
+ base.newInstance()
+ else
+ @isNew = true
+ this
+
+
+
+
+ ¶
+
+ Grab the reference to the superclass's implementation of the current +method. + + + superReference: (o) ->
+ method = o.scope.namedMethod()
+ if method?.klass
+ accesses = [new Access(new Literal '__super__')]
+ accesses.push new Access new Literal 'constructor' if method.static
+ accesses.push new Access new Literal method.name
+ (new Value (new Literal method.klass), accesses).compile o
+ else if method?.ctor
+ "#{method.name}.__super__.constructor"
+ else
+ @error 'cannot call super outside of an instance method.'
+
+
+
+
+ ¶
+
+ The appropriate superThis : (o) ->
+ method = o.scope.method
+ (method and not method.klass and method.context) or "this"
+
+
+
+
+ ¶
+
+ Soaked chained invocations unfold into if/else ternary structures. + + + unfoldSoak: (o) ->
+ if @soak
+ if @variable
+ return ifn if ifn = unfoldSoak o, this, 'variable'
+ [left, rite] = new Value(@variable).cacheReference o
+ else
+ left = new Literal @superReference o
+ rite = new Value left
+ rite = new Call rite, @args
+ rite.isNew = @isNew
+ left = new Literal "typeof #{ left.compile o } === \"function\""
+ return new If left, new Value(rite), soak: yes
+ call = this
+ list = []
+ loop
+ if call.variable instanceof Call
+ list.push call
+ call = call.variable
+ continue
+ break unless call.variable instanceof Value
+ list.push call
+ break unless (call = call.variable.base) instanceof Call
+ for call in list.reverse()
+ if ifn
+ if call.variable instanceof Call
+ call.variable = ifn
+ else
+ call.variable.base = ifn
+ ifn = unfoldSoak o, call, 'variable'
+ ifn
+
+
+
+
+ ¶
+
+ Compile a vanilla function call. + + + compileNode: (o) ->
+ @variable?.front = @front
+ compiledArray = Splat.compileSplattedArray o, @args, true
+ if compiledArray.length
+ return @compileSplat o, compiledArray
+ compiledArgs = []
+ for arg, argIndex in @args
+ if argIndex then compiledArgs.push @makeCode ", "
+ compiledArgs.push (arg.compileToFragments o, LEVEL_LIST)...
+
+ fragments = []
+ if @isSuper
+ preface = @superReference(o) + ".call(#{@superThis(o)}"
+ if compiledArgs.length then preface += ", "
+ fragments.push @makeCode preface
+ else
+ if @isNew then fragments.push @makeCode 'new '
+ fragments.push @variable.compileToFragments(o, LEVEL_ACCESS)...
+ fragments.push @makeCode "("
+ fragments.push compiledArgs...
+ fragments.push @makeCode ")"
+ fragments
+
+
+ ¶
+
+ If you call a function with a splat, it's converted into a JavaScript
splatArgs is an array of CodeFragments to put into the 'apply'. | compileSplat: (o, splatArgs) ->
- if @isSuper
- return [].concat @makeCode("#{ @superReference o }.apply(#{@superThis(o)}, "),
- splatArgs, @makeCode(")")
-
- if @isNew
- idt = @tab + TAB
- return [].concat @makeCode("""
- (function(func, args, ctor) {
- #{idt}ctor.prototype = func.prototype;
- #{idt}var child = new ctor, result = func.apply(child, args);
- #{idt}return Object(result) === result ? result : child;
- #{@tab}})("""),
- (@variable.compileToFragments o, LEVEL_LIST),
- @makeCode(", "), splatArgs, @makeCode(", function(){})")
-
- answer = []
- base = new Value @variable
- if (name = base.properties.pop()) and base.isComplex()
- ref = o.scope.freeVariable 'ref'
- answer = answer.concat @makeCode("(#{ref} = "),
- (base.compileToFragments o, LEVEL_LIST),
- @makeCode(")"),
- name.compileToFragments(o)
- else
- fun = base.compileToFragments o, LEVEL_ACCESS
- fun = @wrapInBraces fun if SIMPLENUM.test fragmentsToText fun
- if name
- ref = fragmentsToText fun
- fun.push (name.compileToFragments o)...
- else
- ref = 'null'
- answer = answer.concat fun
- answer = answer.concat @makeCode(".apply(#{ref}, "), splatArgs, @makeCode(")") |
Extends | |
Node to extend an object's prototype with an ancestor object. +If it's a constructor, then things get real tricky. We have to inject an +inner constructor in order to be able to pass the varargs. + + +splatArgs is an array of CodeFragments to put into the 'apply'. + + + + + compileSplat: (o, splatArgs) ->
+ if @isSuper
+ return [].concat @makeCode("#{ @superReference o }.apply(#{@superThis(o)}, "),
+ splatArgs, @makeCode(")")
+
+ if @isNew
+ idt = @tab + TAB
+ return [].concat @makeCode("""
+ (function(func, args, ctor) {
+ #{idt}ctor.prototype = func.prototype;
+ #{idt}var child = new ctor, result = func.apply(child, args);
+ #{idt}return Object(result) === result ? result : child;
+ #{@tab}})("""),
+ (@variable.compileToFragments o, LEVEL_LIST),
+ @makeCode(", "), splatArgs, @makeCode(", function(){})")
+
+ answer = []
+ base = new Value @variable
+ if (name = base.properties.pop()) and base.isComplex()
+ ref = o.scope.freeVariable 'ref'
+ answer = answer.concat @makeCode("(#{ref} = "),
+ (base.compileToFragments o, LEVEL_LIST),
+ @makeCode(")"),
+ name.compileToFragments(o)
+ else
+ fun = base.compileToFragments o, LEVEL_ACCESS
+ fun = @wrapInBraces fun if SIMPLENUM.test fragmentsToText fun
+ if name
+ ref = fragmentsToText fun
+ fun.push (name.compileToFragments o)...
+ else
+ ref = 'null'
+ answer = answer.concat fun
+ answer = answer.concat @makeCode(".apply(#{ref}, "), splatArgs, @makeCode(")")
+
+
+
+
+ ¶
+
+ Extends+ +
+
+
+ ¶
+
+ Node to extend an object's prototype with an ancestor object.
After | exports.Extends = class Extends extends Base
- constructor: (@child, @parent) ->
-
- children: ['child', 'parent'] |
Hooks one constructor into another's prototype chain. | compileToFragments: (o) ->
- new Call(new Value(new Literal utility 'extends'), [@child, @parent]).compileToFragments o |
Access | |
A | exports.Access = class Access extends Base
- constructor: (@name, tag) ->
- @name.asKey = yes
- @soak = tag is 'soak'
-
- children: ['name']
-
- compileToFragments: (o) ->
- name = @name.compileToFragments o
- if IDENTIFIER.test fragmentsToText name
- name.unshift @makeCode "."
- else
- name.unshift @makeCode "["
- name.push @makeCode "]"
- name
-
- isComplex: NO |
Index | |
A | exports.Index = class Index extends Base
- constructor: (@index) ->
-
- children: ['index']
-
- compileToFragments: (o) ->
- [].concat @makeCode("["), (@index.compileToFragments o, LEVEL_PAREN), @makeCode("]")
-
- isComplex: ->
- @index.isComplex() |
Range | |
A range literal. Ranges can be used to extract portions (slices) of arrays, +Closure Library. + + + + +exports.Extends = class Extends extends Base
+ constructor: (@child, @parent) ->
+
+ children: ['child', 'parent']
+
+
+
+
+ ¶
+
+ Hooks one constructor into another's prototype chain. + + + compileToFragments: (o) ->
+ new Call(new Value(new Literal utility 'extends'), [@child, @parent]).compileToFragments o
+
+
+
+
+ ¶
+
+ Access+ +
+
+
+
+
+ ¶
+
+ A exports.Access = class Access extends Base
+ constructor: (@name, tag) ->
+ @name.asKey = yes
+ @soak = tag is 'soak'
+
+ children: ['name']
+
+ compileToFragments: (o) ->
+ name = @name.compileToFragments o
+ if IDENTIFIER.test fragmentsToText name
+ name.unshift @makeCode "."
+ else
+ name.unshift @makeCode "["
+ name.push @makeCode "]"
+ name
+
+ isComplex: NO
+
+
+
+
+ ¶
+
+ Index+ +
+
+
+
+
+ ¶
+
+ A exports.Index = class Index extends Base
+ constructor: (@index) ->
+
+ children: ['index']
+
+ compileToFragments: (o) ->
+ [].concat @makeCode("["), @index.compileToFragments(o, LEVEL_PAREN), @makeCode("]")
+
+ isComplex: ->
+ @index.isComplex()
+
+
+
+
+ ¶
+
+ Range+ +
+
+
+ ¶
+
+ A range literal. Ranges can be used to extract portions (slices) of arrays, to specify a range for comprehensions, or as a value, to be expanded into the -corresponding array of integers at runtime. | exports.Range = class Range extends Base
-
- children: ['from', 'to']
-
- constructor: (@from, @to, tag) ->
- @exclusive = tag is 'exclusive'
- @equals = if @exclusive then '' else '=' |
Compiles the range's source variables -- where it starts and where it ends. -But only if they need to be cached to avoid double evaluation. | compileVariables: (o) ->
- o = merge o, top: true
- [@fromC, @fromVar] = @cacheToCodeFragments @from.cache o, LEVEL_LIST
- [@toC, @toVar] = @cacheToCodeFragments @to.cache o, LEVEL_LIST
- [@step, @stepVar] = @cacheToCodeFragments step.cache o, LEVEL_LIST if step = del o, 'step'
- [@fromNum, @toNum] = [@fromVar.match(SIMPLENUM), @toVar.match(SIMPLENUM)]
- @stepNum = @stepVar.match(SIMPLENUM) if @stepVar |
When compiled normally, the range returns the contents of the for loop -needed to iterate over the values in the range. Used by comprehensions. | compileNode: (o) ->
- @compileVariables o unless @fromVar
- return @compileArray(o) unless o.index |
Set up endpoints. | known = @fromNum and @toNum
- idx = del o, 'index'
- idxName = del o, 'name'
- namedIndex = idxName and idxName isnt idx
- varPart = "#{idx} = #{@fromC}"
- varPart += ", #{@toC}" if @toC isnt @toVar
- varPart += ", #{@step}" if @step isnt @stepVar
- [lt, gt] = ["#{idx} <#{@equals}", "#{idx} >#{@equals}"] |
Generate the condition. | condPart = if @stepNum
- if +@stepNum > 0 then "#{lt} #{@toVar}" else "#{gt} #{@toVar}"
- else if known
- [from, to] = [+@fromNum, +@toNum]
- if from <= to then "#{lt} #{to}" else "#{gt} #{to}"
- else
- cond = if @stepVar then "#{@stepVar} > 0" else "#{@fromVar} <= #{@toVar}"
- "#{cond} ? #{lt} #{@toVar} : #{gt} #{@toVar}" |
Generate the step. | stepPart = if @stepVar
- "#{idx} += #{@stepVar}"
- else if known
- if namedIndex
- if from <= to then "++#{idx}" else "--#{idx}"
- else
- if from <= to then "#{idx}++" else "#{idx}--"
- else
- if namedIndex
- "#{cond} ? ++#{idx} : --#{idx}"
- else
- "#{cond} ? #{idx}++ : #{idx}--"
-
- varPart = "#{idxName} = #{varPart}" if namedIndex
- stepPart = "#{idxName} = #{stepPart}" if namedIndex |
The final loop body. | [@makeCode "#{varPart}; #{condPart}; #{stepPart}"] |
When used as a value, expand the range into the equivalent array. | compileArray: (o) ->
- if @fromNum and @toNum and Math.abs(@fromNum - @toNum) <= 20
- range = [+@fromNum..+@toNum]
- range.pop() if @exclusive
- return [@makeCode "[#{ range.join(', ') }]"]
- idt = @tab + TAB
- i = o.scope.freeVariable 'i'
- result = o.scope.freeVariable 'results'
- pre = "\n#{idt}#{result} = [];"
- if @fromNum and @toNum
- o.index = i
- body = fragmentsToText @compileNode o
- else
- vars = "#{i} = #{@fromC}" + if @toC isnt @toVar then ", #{@toC}" else ''
- cond = "#{@fromVar} <= #{@toVar}"
- body = "var #{vars}; #{cond} ? #{i} <#{@equals} #{@toVar} : #{i} >#{@equals} #{@toVar}; #{cond} ? #{i}++ : #{i}--"
- post = "{ #{result}.push(#{i}); }\n#{idt}return #{result};\n#{o.indent}"
- hasArgs = (node) -> node?.contains (n) -> n instanceof Literal and n.value is 'arguments' and not n.asKey
- args = ', arguments' if hasArgs(@from) or hasArgs(@to)
- [@makeCode "(function() {#{pre}\n#{idt}for (#{body})#{post}}).apply(this#{args ? ''})"] |
Slice | |
An array slice literal. Unlike JavaScript's exports.Range = class Range extends Base
+
+ children: ['from', 'to']
+
+ constructor: (@from, @to, tag) ->
+ @exclusive = tag is 'exclusive'
+ @equals = if @exclusive then '' else '='
+
+
+
+
+ ¶
+
+ Compiles the range's source variables -- where it starts and where it ends. +But only if they need to be cached to avoid double evaluation. + + + compileVariables: (o) ->
+ o = merge o, top: true
+ [@fromC, @fromVar] = @cacheToCodeFragments @from.cache o, LEVEL_LIST
+ [@toC, @toVar] = @cacheToCodeFragments @to.cache o, LEVEL_LIST
+ [@step, @stepVar] = @cacheToCodeFragments step.cache o, LEVEL_LIST if step = del o, 'step'
+ [@fromNum, @toNum] = [@fromVar.match(SIMPLENUM), @toVar.match(SIMPLENUM)]
+ @stepNum = @stepVar.match(SIMPLENUM) if @stepVar
+
+
+
+
+ ¶
+
+ When compiled normally, the range returns the contents of the for loop +needed to iterate over the values in the range. Used by comprehensions. + + + compileNode: (o) ->
+ @compileVariables o unless @fromVar
+ return @compileArray(o) unless o.index
+
+
+
+
+ ¶
+
+ Set up endpoints. + + + known = @fromNum and @toNum
+ idx = del o, 'index'
+ idxName = del o, 'name'
+ namedIndex = idxName and idxName isnt idx
+ varPart = "#{idx} = #{@fromC}"
+ varPart += ", #{@toC}" if @toC isnt @toVar
+ varPart += ", #{@step}" if @step isnt @stepVar
+ [lt, gt] = ["#{idx} <#{@equals}", "#{idx} >#{@equals}"]
+
+
+
+
+ ¶
+
+ Generate the condition. + + + condPart = if @stepNum
+ if +@stepNum > 0 then "#{lt} #{@toVar}" else "#{gt} #{@toVar}"
+ else if known
+ [from, to] = [+@fromNum, +@toNum]
+ if from <= to then "#{lt} #{to}" else "#{gt} #{to}"
+ else
+ cond = if @stepVar then "#{@stepVar} > 0" else "#{@fromVar} <= #{@toVar}"
+ "#{cond} ? #{lt} #{@toVar} : #{gt} #{@toVar}"
+
+
+
+
+ ¶
+
+ Generate the step. + + + stepPart = if @stepVar
+ "#{idx} += #{@stepVar}"
+ else if known
+ if namedIndex
+ if from <= to then "++#{idx}" else "--#{idx}"
+ else
+ if from <= to then "#{idx}++" else "#{idx}--"
+ else
+ if namedIndex
+ "#{cond} ? ++#{idx} : --#{idx}"
+ else
+ "#{cond} ? #{idx}++ : #{idx}--"
+
+ varPart = "#{idxName} = #{varPart}" if namedIndex
+ stepPart = "#{idxName} = #{stepPart}" if namedIndex
+
+
+
+
+ ¶
+
+ The final loop body. + + + [@makeCode "#{varPart}; #{condPart}; #{stepPart}"]
+
+
+
+
+ ¶
+
+ When used as a value, expand the range into the equivalent array. + + + compileArray: (o) ->
+ if @fromNum and @toNum and Math.abs(@fromNum - @toNum) <= 20
+ range = [+@fromNum..+@toNum]
+ range.pop() if @exclusive
+ return [@makeCode "[#{ range.join(', ') }]"]
+ idt = @tab + TAB
+ i = o.scope.freeVariable 'i'
+ result = o.scope.freeVariable 'results'
+ pre = "\n#{idt}#{result} = [];"
+ if @fromNum and @toNum
+ o.index = i
+ body = fragmentsToText @compileNode o
+ else
+ vars = "#{i} = #{@fromC}" + if @toC isnt @toVar then ", #{@toC}" else ''
+ cond = "#{@fromVar} <= #{@toVar}"
+ body = "var #{vars}; #{cond} ? #{i} <#{@equals} #{@toVar} : #{i} >#{@equals} #{@toVar}; #{cond} ? #{i}++ : #{i}--"
+ post = "{ #{result}.push(#{i}); }\n#{idt}return #{result};\n#{o.indent}"
+ hasArgs = (node) -> node?.contains (n) -> n instanceof Literal and n.value is 'arguments' and not n.asKey
+ args = ', arguments' if hasArgs(@from) or hasArgs(@to)
+ [@makeCode "(function() {#{pre}\n#{idt}for (#{body})#{post}}).apply(this#{args ? ''})"]
+
+
+
+
+ ¶
+
+ Slice+ +
+
+
+ ¶
+
+ An array slice literal. Unlike JavaScript's | exports.Slice = class Slice extends Base
-
- children: ['range']
-
- constructor: (@range) ->
- super() |
We have to be careful when trying to slice through the end of the array, +is the index of the beginning. + + + + +exports.Slice = class Slice extends Base
+
+ children: ['range']
+
+ constructor: (@range) ->
+ super()
+
+
+ ¶
+
+ We have to be careful when trying to slice through the end of the array,
| compileNode: (o) ->
- {to, from} = @range
- fromCompiled = from and from.compileToFragments(o, LEVEL_PAREN) or [@makeCode '0'] |
TODO: jwalton - move this into the 'if'? | if to
- compiled = to.compileToFragments o, LEVEL_PAREN
- compiledText = fragmentsToText compiled
- if not (not @range.exclusive and +compiledText is -1)
- toStr = ', ' + if @range.exclusive
- compiledText
- else if SIMPLENUM.test compiledText
- "#{+compiledText + 1}"
- else
- compiled = to.compileToFragments o, LEVEL_ACCESS
- "+#{fragmentsToText compiled} + 1 || 9e9"
- [@makeCode ".slice(#{ fragmentsToText fromCompiled }#{ toStr or '' })"] |
Obj | |
An object literal, nothing fancy. | exports.Obj = class Obj extends Base
- constructor: (props, @generated = false) ->
- @objects = @properties = props or []
-
- children: ['properties']
-
- compileNode: (o) ->
- props = @properties
- return [@makeCode(if @front then '({})' else '{}')] unless props.length
- if @generated
- for node in props when node instanceof Value
- throw new Error 'cannot have an implicit value in an implicit object'
- idt = o.indent += TAB
- lastNoncom = @lastNonComment @properties
- answer = []
- for prop, i in props
- join = if i is props.length - 1
- ''
- else if prop is lastNoncom or prop instanceof Comment
- '\n'
- else
- ',\n'
- indent = if prop instanceof Comment then '' else idt
- if prop instanceof Value and prop.this
- prop = new Assign prop.properties[0].name, prop, 'object'
- if prop not instanceof Comment
- if prop not instanceof Assign
- prop = new Assign prop, prop, 'object'
- (prop.variable.base or prop.variable).asKey = yes
- if indent then answer.push @makeCode indent
- answer.push prop.compileToFragments(o, LEVEL_TOP)...
- if join then answer.push @makeCode join
- answer.unshift @makeCode "{#{ props.length and '\n' }"
- answer.push @makeCode "#{ props.length and '\n' + @tab }}"
- if @front then @wrapInBraces answer else answer
-
- assigns: (name) ->
- for prop in @properties when prop.assigns name then return yes
- no |
Arr | |
An array literal. | exports.Arr = class Arr extends Base
- constructor: (objs) ->
- @objects = objs or []
-
- children: ['objects']
-
- compileNode: (o) ->
- return [@makeCode '[]'] unless @objects.length
- o.indent += TAB
- answer = Splat.compileSplattedArray o, @objects
- return answer if answer.length
-
- answer = []
- compiledObjs = (obj.compileToFragments o, LEVEL_LIST for obj in @objects)
- for fragments, index in compiledObjs
- if index
- answer.push @makeCode ", "
- answer.push fragments...
- if (fragmentsToText answer).indexOf('\n') >= 0
- answer.unshift @makeCode "[\n#{o.indent}"
- answer.push @makeCode "\n#{@tab}]"
- else
- answer.unshift @makeCode "["
- answer.push @makeCode "]"
- answer
-
- assigns: (name) ->
- for obj in @objects when obj.assigns name then return yes
- no |
Class | |
The CoffeeScript class definition.
+ compileNode: (o) ->
+ {to, from} = @range
+ fromCompiled = from and from.compileToFragments(o, LEVEL_PAREN) or [@makeCode '0']
+
+
+
+
+ ¶
+
+ TODO: jwalton - move this into the 'if'? + + + if to
+ compiled = to.compileToFragments o, LEVEL_PAREN
+ compiledText = fragmentsToText compiled
+ if not (not @range.exclusive and +compiledText is -1)
+ toStr = ', ' + if @range.exclusive
+ compiledText
+ else if SIMPLENUM.test compiledText
+ "#{+compiledText + 1}"
+ else
+ compiled = to.compileToFragments o, LEVEL_ACCESS
+ "+#{fragmentsToText compiled} + 1 || 9e9"
+ [@makeCode ".slice(#{ fragmentsToText fromCompiled }#{ toStr or '' })"]
+
+
+
+
+ ¶
+
+ Obj+ +
+
+
+
+
+ ¶
+
+ An object literal, nothing fancy. + + +exports.Obj = class Obj extends Base
+ constructor: (props, @generated = false) ->
+ @objects = @properties = props or []
+
+ children: ['properties']
+
+ compileNode: (o) ->
+ props = @properties
+ return [@makeCode(if @front then '({})' else '{}')] unless props.length
+ if @generated
+ for node in props when node instanceof Value
+ node.error 'cannot have an implicit value in an implicit object'
+ idt = o.indent += TAB
+ lastNoncom = @lastNonComment @properties
+ answer = []
+ for prop, i in props
+ join = if i is props.length - 1
+ ''
+ else if prop is lastNoncom or prop instanceof Comment
+ '\n'
+ else
+ ',\n'
+ indent = if prop instanceof Comment then '' else idt
+ if prop instanceof Assign and prop.variable instanceof Value and prop.variable.hasProperties()
+ prop.variable.error 'Invalid object key'
+ if prop instanceof Value and prop.this
+ prop = new Assign prop.properties[0].name, prop, 'object'
+ if prop not instanceof Comment
+ if prop not instanceof Assign
+ prop = new Assign prop, prop, 'object'
+ (prop.variable.base or prop.variable).asKey = yes
+ if indent then answer.push @makeCode indent
+ answer.push prop.compileToFragments(o, LEVEL_TOP)...
+ if join then answer.push @makeCode join
+ answer.unshift @makeCode "{#{ props.length and '\n' }"
+ answer.push @makeCode "#{ props.length and '\n' + @tab }}"
+ if @front then @wrapInBraces answer else answer
+
+ assigns: (name) ->
+ for prop in @properties when prop.assigns name then return yes
+ no
+
+
+
+
+ ¶
+
+ Arr+ +
+
+
+
+
+ ¶
+
+ An array literal. + + +exports.Arr = class Arr extends Base
+ constructor: (objs) ->
+ @objects = objs or []
+
+ children: ['objects']
+
+ compileNode: (o) ->
+ return [@makeCode '[]'] unless @objects.length
+ o.indent += TAB
+ answer = Splat.compileSplattedArray o, @objects
+ return answer if answer.length
+
+ answer = []
+ compiledObjs = (obj.compileToFragments o, LEVEL_LIST for obj in @objects)
+ for fragments, index in compiledObjs
+ if index
+ answer.push @makeCode ", "
+ answer.push fragments...
+ if fragmentsToText(answer).indexOf('\n') >= 0
+ answer.unshift @makeCode "[\n#{o.indent}"
+ answer.push @makeCode "\n#{@tab}]"
+ else
+ answer.unshift @makeCode "["
+ answer.push @makeCode "]"
+ answer
+
+ assigns: (name) ->
+ for obj in @objects when obj.assigns name then return yes
+ no
+
+
+
+
+ ¶
+
+ Class+ +
+
+
+ ¶
+
+ The CoffeeScript class definition. Initialize a Class with its name, an optional superclass, and a -list of prototype property assignments. | exports.Class = class Class extends Base
- constructor: (@variable, @parent, @body = new Block) ->
- @boundFuncs = []
- @body.classBody = yes
-
- children: ['variable', 'parent', 'body'] |
Figure out the appropriate name for the constructor function of this class. | determineName: ->
- return null unless @variable
- decl = if tail = last @variable.properties
- tail instanceof Access and tail.name.value
- else
- @variable.base.value
- if decl in STRICT_PROSCRIBED
- throw SyntaxError "variable name may not be #{decl}"
- decl and= IDENTIFIER.test(decl) and decl |
For all | setContext: (name) ->
- @body.traverseChildren false, (node) ->
- return false if node.classBody
- if node instanceof Literal and node.value is 'this'
- node.value = name
- else if node instanceof Code
- node.klass = name
- node.context = name if node.bound |
Ensure that all functions bound to the instance are proxied in the -constructor. | addBoundFunctions: (o) ->
- if @boundFuncs.length
- o.scope.assign '_this', 'this'
- for [name, func] in @boundFuncs
- lhs = new Value (new Literal "this"), [new Access name]
- body = new Block [new Return new Literal "#{@ctor.name}.prototype.#{name.value}.apply(_this, arguments)"]
- rhs = new Code func.params, body, 'boundfunc'
- bound = new Assign lhs, rhs
-
- @ctor.body.unshift bound |
{base} = assign.variable -lhs = (new Value (new Literal "this"), [new Access base]).compile o -@ctor.body.unshift new Literal """#{lhs} = function() { - -{o.indent} return #{@ctor.name}.prototype.#{base.value}.apply(_this, arguments);- -{o.indent}}\n- -""" | return |
Merge the properties from a top-level object as prototypal properties -on the class. | addProperties: (node, name, o) ->
- props = node.base.properties[..]
- exprs = while assign = props.shift()
- if assign instanceof Assign
- base = assign.variable.base
- delete assign.context
- func = assign.value
- if base.value is 'constructor'
- if @ctor
- throw new Error 'cannot define more than one constructor in a class'
- if func.bound
- throw new Error 'cannot define a constructor as a bound function'
- if func instanceof Code
- assign = @ctor = func
- else
- @externalCtor = o.scope.freeVariable 'class'
- assign = new Assign new Literal(@externalCtor), func
- else
- if assign.variable.this
- func.static = yes
- if func.bound
- func.context = name
- else
- assign.variable = new Value(new Literal(name), [(new Access new Literal 'prototype'), new Access base ])
- if func instanceof Code and func.bound
- @boundFuncs.push [base, func]
- func.bound = no
- assign
- compact exprs |
Walk the body of the class, looking for prototype properties to be converted. | walkBody: (name, o) ->
- @traverseChildren false, (child) =>
- cont = true
- return false if child instanceof Class
- if child instanceof Block
- for node, i in exps = child.expressions
- if node instanceof Value and node.isObject(true)
- cont = false
- exps[i] = @addProperties node, name, o
- child.expressions = exps = flatten exps
- cont and child not instanceof Class |
exports.Class = class Class extends Base
+ constructor: (@variable, @parent, @body = new Block) ->
+ @boundFuncs = []
+ @body.classBody = yes
+
+ children: ['variable', 'parent', 'body']
+
+
+
+
+ ¶
+
+ Figure out the appropriate name for the constructor function of this class. + + + determineName: ->
+ return null unless @variable
+ decl = if tail = last @variable.properties
+ tail instanceof Access and tail.name.value
+ else
+ @variable.base.value
+ if decl in STRICT_PROSCRIBED
+ @variable.error "class variable name may not be #{decl}"
+ decl and= IDENTIFIER.test(decl) and decl
+
+
+
+
+ ¶
+
+ For all setContext: (name) ->
+ @body.traverseChildren false, (node) ->
+ return false if node.classBody
+ if node instanceof Literal and node.value is 'this'
+ node.value = name
+ else if node instanceof Code
+ node.klass = name
+ node.context = name if node.bound
+
+
+
+
+ ¶
+
+ Ensure that all functions bound to the instance are proxied in the +constructor. + + + addBoundFunctions: (o) ->
+ for bvar in @boundFuncs
+ lhs = (new Value (new Literal "this"), [new Access bvar]).compile o
+ @ctor.body.unshift new Literal "#{lhs} = #{utility 'bind'}(#{lhs}, this)"
+ return
+
+
+
+
+ ¶
+
+ Merge the properties from a top-level object as prototypal properties +on the class. + + + addProperties: (node, name, o) ->
+ props = node.base.properties[..]
+ exprs = while assign = props.shift()
+ if assign instanceof Assign
+ base = assign.variable.base
+ delete assign.context
+ func = assign.value
+ if base.value is 'constructor'
+ if @ctor
+ assign.error 'cannot define more than one constructor in a class'
+ if func.bound
+ assign.error 'cannot define a constructor as a bound function'
+ if func instanceof Code
+ assign = @ctor = func
+ else
+ @externalCtor = o.scope.freeVariable 'class'
+ assign = new Assign new Literal(@externalCtor), func
+ else
+ if assign.variable.this
+ func.static = yes
+ if func.bound
+ func.context = name
+ else
+ assign.variable = new Value(new Literal(name), [(new Access new Literal 'prototype'), new Access base])
+ if func instanceof Code and func.bound
+ @boundFuncs.push base
+ func.bound = no
+ assign
+ compact exprs
+
+
+
+
+ ¶
+
+ Walk the body of the class, looking for prototype properties to be converted. + + + walkBody: (name, o) ->
+ @traverseChildren false, (child) =>
+ cont = true
+ return false if child instanceof Class
+ if child instanceof Block
+ for node, i in exps = child.expressions
+ if node instanceof Value and node.isObject(true)
+ cont = false
+ exps[i] = @addProperties node, name, o
+ child.expressions = exps = flatten exps
+ cont and child not instanceof Class
+
+
+ ¶
+
+
| hoistDirectivePrologue: ->
- index = 0
- {expressions} = @body
- ++index while (node = expressions[index]) and node instanceof Comment or
- node instanceof Value and node.isString()
- @directives = expressions.splice 0, index |
Make sure that a constructor is defined for the class, and properly -configured. | ensureConstructor: (name) ->
- if not @ctor
- @ctor = new Code
- @ctor.body.push new Literal "#{name}.__super__.constructor.apply(this, arguments)" if @parent
- @ctor.body.push new Literal "#{@externalCtor}.apply(this, arguments)" if @externalCtor
- @ctor.body.makeReturn()
- @body.expressions.unshift @ctor
- @ctor.ctor = @ctor.name = name
- @ctor.klass = null
- @ctor.noReturn = yes |
Instead of generating the JavaScript string directly, we build up the
+above the hoistDirectivePrologue: ->
+ index = 0
+ {expressions} = @body
+ ++index while (node = expressions[index]) and node instanceof Comment or
+ node instanceof Value and node.isString()
+ @directives = expressions.splice 0, index
+
+
+
+
+ ¶
+
+ Make sure that a constructor is defined for the class, and properly +configured. + + + ensureConstructor: (name, o) ->
+ missing = not @ctor
+ @ctor or= new Code
+ @ctor.ctor = @ctor.name = name
+ @ctor.klass = null
+ @ctor.noReturn = yes
+ if missing
+ superCall = new Literal "#{name}.__super__.constructor.apply(this, arguments)" if @parent
+ superCall = new Literal "#{@externalCtor}.apply(this, arguments)" if @externalCtor
+ if superCall
+ ref = new Literal o.scope.freeVariable 'ref'
+ @ctor.body.unshift new Assign ref, superCall
+ @addBoundFunctions o
+ if superCall
+ @ctor.body.push ref
+ @ctor.body.makeReturn()
+ @body.expressions.unshift @ctor
+ else
+ @addBoundFunctions o
+
+
+ ¶
+
+ Instead of generating the JavaScript string directly, we build up the equivalent syntax tree and compile that, in pieces. You can see the -constructor, property assignments, and inheritance getting built out below. | compileNode: (o) ->
- decl = @determineName()
- name = decl or '_Class'
- name = "_#{name}" if name.reserved
- lname = new Literal name
-
- @hoistDirectivePrologue()
- @setContext name
- @walkBody name, o
- @ensureConstructor name
- @body.spaced = yes
- @body.expressions.unshift @ctor unless @ctor instanceof Code
- @body.expressions.push lname
- @body.expressions.unshift @directives...
- @addBoundFunctions o
-
- call = Closure.wrap @body
-
- if @parent
- @superClass = new Literal o.scope.freeVariable 'super', no
- @body.expressions.unshift new Extends lname, @superClass
- call.args.push @parent
- params = call.variable.params or call.variable.base.params
- params.push new Param @superClass
-
- klass = new Parens call, yes
- klass = new Assign @variable, klass if @variable
- klass.compileToFragments o |
Assign | |
The Assign is used to assign a local variable to value, or to set the -property of an object -- including within object literals. | exports.Assign = class Assign extends Base
- constructor: (@variable, @value, @context, options) ->
- @param = options and options.param
- @subpattern = options and options.subpattern
- forbidden = (name = @variable.unwrapAll().value) in STRICT_PROSCRIBED
- if forbidden and @context isnt 'object'
- throw SyntaxError "variable name may not be \"#{name}\""
-
- children: ['variable', 'value']
-
- isStatement: (o) ->
- o?.level is LEVEL_TOP and @context? and "?" in @context
-
- assigns: (name) ->
- @[if @context is 'object' then 'value' else 'variable'].assigns name
-
- unfoldSoak: (o) ->
- unfoldSoak o, this, 'variable' |
Compile an assignment, delegating to compileNode: (o) ->
+ decl = @determineName()
+ name = decl or '_Class'
+ name = "_#{name}" if name.reserved
+ lname = new Literal name
+
+ @hoistDirectivePrologue()
+ @setContext name
+ @walkBody name, o
+ @ensureConstructor name, o
+ @body.spaced = yes
+ @body.expressions.unshift @ctor unless @ctor instanceof Code
+ @body.expressions.push lname
+ @body.expressions.unshift @directives...
+
+ call = Closure.wrap @body
+
+ if @parent
+ @superClass = new Literal o.scope.freeVariable 'super', no
+ @body.expressions.unshift new Extends lname, @superClass
+ call.args.push @parent
+ params = call.variable.params or call.variable.base.params
+ params.push new Param @superClass
+
+ klass = new Parens call, yes
+ klass = new Assign @variable, klass if @variable
+ klass.compileToFragments o
+
+
+
+
+ ¶
+
+ Assign+ +
+
+
+
+
+ ¶
+
+ The Assign is used to assign a local variable to value, or to set the +property of an object -- including within object literals. + + +exports.Assign = class Assign extends Base
+ constructor: (@variable, @value, @context, options) ->
+ @param = options and options.param
+ @subpattern = options and options.subpattern
+ forbidden = (name = @variable.unwrapAll().value) in STRICT_PROSCRIBED
+ if forbidden and @context isnt 'object'
+ @variable.error "variable name may not be \"#{name}\""
+
+ children: ['variable', 'value']
+
+ isStatement: (o) ->
+ o?.level is LEVEL_TOP and @context? and "?" in @context
+
+ assigns: (name) ->
+ @[if @context is 'object' then 'value' else 'variable'].assigns name
+
+ unfoldSoak: (o) ->
+ unfoldSoak o, this, 'variable'
+
+
+ ¶
+
+ Compile an assignment, delegating to | compileNode: (o) ->
- if isValue = @variable instanceof Value
- return @compilePatternMatch o if @variable.isArray() or @variable.isObject()
- return @compileSplice o if @variable.isSplice()
- return @compileConditional o if @context in ['||=', '&&=', '?=']
- compiledName = @variable.compileToFragments o, LEVEL_LIST
- name = fragmentsToText compiledName
- unless @context
- unless (varBase = @variable.unwrapAll()).isAssignable()
- throw SyntaxError "\"#{ @variable.compile o }\" cannot be assigned."
- unless varBase.hasProperties?()
- if @param
- o.scope.add name, 'var'
- else
- o.scope.find name
- if @value instanceof Code and match = METHOD_DEF.exec name
- @value.klass = match[1] if match[1]
- @value.name = match[2] ? match[3] ? match[4] ? match[5]
- val = @value.compileToFragments o, LEVEL_LIST
- return (compiledName.concat @makeCode(": "), val) if @context is 'object'
- answer = compiledName.concat @makeCode(" #{ @context or '=' } "), val
- if o.level <= LEVEL_LIST then answer else @wrapInBraces answer |
Brief implementation of recursive pattern matching, when assigning array or +we've been assigned to, for correct internal references. If the variable +has not been seen yet within the current scope, declare it. + + + + + compileNode: (o) ->
+ if isValue = @variable instanceof Value
+ return @compilePatternMatch o if @variable.isArray() or @variable.isObject()
+ return @compileSplice o if @variable.isSplice()
+ return @compileConditional o if @context in ['||=', '&&=', '?=']
+ compiledName = @variable.compileToFragments o, LEVEL_LIST
+ name = fragmentsToText compiledName
+ unless @context
+ varBase = @variable.unwrapAll()
+ unless varBase.isAssignable()
+ @variable.error "\"#{@variable.compile o}\" cannot be assigned"
+ unless varBase.hasProperties?()
+ if @param
+ o.scope.add name, 'var'
+ else
+ o.scope.find name
+ if @value instanceof Code and match = METHOD_DEF.exec name
+ @value.klass = match[1] if match[1]
+ @value.name = match[2] ? match[3] ? match[4] ? match[5]
+ val = @value.compileToFragments o, LEVEL_LIST
+ return (compiledName.concat @makeCode(": "), val) if @context is 'object'
+ answer = compiledName.concat @makeCode(" #{ @context or '=' } "), val
+ if o.level <= LEVEL_LIST then answer else @wrapInBraces answer
+
+
+ ¶
+
+ Brief implementation of recursive pattern matching, when assigning array or object literals to a value. Peeks at their properties to assign inner names. See the ECMAScript Harmony Wiki -for details. | compilePatternMatch: (o) ->
- top = o.level is LEVEL_TOP
- {value} = this
- {objects} = @variable.base
- unless olen = objects.length
- code = value.compileToFragments o
- return if o.level >= LEVEL_OP then @wrapInBraces code else code
- isObject = @variable.isObject()
- if top and olen is 1 and (obj = objects[0]) not instanceof Splat |
Unroll simplest cases: | if obj instanceof Assign
- {variable: {base: idx}, value: obj} = obj
- else
- idx = if isObject
- if obj.this then obj.properties[0].name else obj
- else
- new Literal 0
- acc = IDENTIFIER.test idx.unwrap().value or 0
- value = new Value value
- value.properties.push new (if acc then Access else Index) idx
- if obj.unwrap().value in RESERVED
- throw new SyntaxError "assignment to a reserved word: #{obj.compile o} = #{value.compile o}"
- return new Assign(obj, value, null, param: @param).compileToFragments o, LEVEL_TOP
- vvar = value.compileToFragments o, LEVEL_LIST
- vvarText = fragmentsToText vvar
- assigns = []
- splat = false |
Make vvar into a simple variable if it isn't already. | if not IDENTIFIER.test(vvarText) or @variable.assigns(vvarText)
- assigns.push [@makeCode("#{ ref = o.scope.freeVariable 'ref' } = "), vvar...]
- vvar = [@makeCode ref]
- vvarText = ref
- for obj, i in objects |
A regular array pattern-match. | idx = i
- if isObject
- if obj instanceof Assign |
A regular object pattern-match. | {variable: {base: idx}, value: obj} = obj
- else |
A shorthand | if obj.base instanceof Parens
- [obj, idx] = new Value(obj.unwrapAll()).cacheReference o
- else
- idx = if obj.this then obj.properties[0].name else obj
- if not splat and obj instanceof Splat
- name = obj.name.unwrap().value
- obj = obj.unwrap()
- val = "#{olen} <= #{vvarText}.length ? #{ utility 'slice' }.call(#{vvarText}, #{i}"
- if rest = olen - i - 1
- ivar = o.scope.freeVariable 'i'
- val += ", #{ivar} = #{vvarText}.length - #{rest}) : (#{ivar} = #{i}, [])"
- else
- val += ") : []"
- val = new Literal val
- splat = "#{ivar}++"
- else
- name = obj.unwrap().value
- if obj instanceof Splat
- obj = obj.name.compileToFragments o
- throw new SyntaxError \
- "multiple splats are disallowed in an assignment: #{obj}..."
- if typeof idx is 'number'
- idx = new Literal splat or idx
- acc = no
- else
- acc = isObject and IDENTIFIER.test idx.unwrap().value or 0
- val = new Value new Literal(vvarText), [new (if acc then Access else Index) idx]
- if name? and name in RESERVED
- throw new SyntaxError "assignment to a reserved word: #{obj.compile o} = #{val.compile o}"
- assigns.push new Assign(obj, val, null, param: @param, subpattern: yes).compileToFragments o, LEVEL_LIST
- assigns.push vvar unless top or @subpattern
- fragments = @joinFragmentArrays assigns, ', '
- if o.level < LEVEL_LIST then fragments else @wrapInBraces fragments |
When compiling a conditional assignment, take care to ensure that the +for details. + + + + + compilePatternMatch: (o) ->
+ top = o.level is LEVEL_TOP
+ {value} = this
+ {objects} = @variable.base
+ unless olen = objects.length
+ code = value.compileToFragments o
+ return if o.level >= LEVEL_OP then @wrapInBraces code else code
+ isObject = @variable.isObject()
+ if top and olen is 1 and (obj = objects[0]) not instanceof Splat
+
+
+
+
+ ¶
+
+ Unroll simplest cases: if obj instanceof Assign
+ {variable: {base: idx}, value: obj} = obj
+ else
+ idx = if isObject
+ if obj.this then obj.properties[0].name else obj
+ else
+ new Literal 0
+ acc = IDENTIFIER.test idx.unwrap().value or 0
+ value = new Value value
+ value.properties.push new (if acc then Access else Index) idx
+ if obj.unwrap().value in RESERVED
+ obj.error "assignment to a reserved word: #{obj.compile o}"
+ return new Assign(obj, value, null, param: @param).compileToFragments o, LEVEL_TOP
+ vvar = value.compileToFragments o, LEVEL_LIST
+ vvarText = fragmentsToText vvar
+ assigns = []
+ splat = false
+
+
+
+
+ ¶
+
+ Make vvar into a simple variable if it isn't already. + + + if not IDENTIFIER.test(vvarText) or @variable.assigns(vvarText)
+ assigns.push [@makeCode("#{ ref = o.scope.freeVariable 'ref' } = "), vvar...]
+ vvar = [@makeCode ref]
+ vvarText = ref
+ for obj, i in objects
+
+
+
+
+ ¶
+
+ A regular array pattern-match. + + + idx = i
+ if isObject
+ if obj instanceof Assign
+
+
+
+
+ ¶
+
+ A regular object pattern-match. + + + {variable: {base: idx}, value: obj} = obj
+ else
+
+
+
+
+ ¶
+
+ A shorthand if obj.base instanceof Parens
+ [obj, idx] = new Value(obj.unwrapAll()).cacheReference o
+ else
+ idx = if obj.this then obj.properties[0].name else obj
+ if not splat and obj instanceof Splat
+ name = obj.name.unwrap().value
+ obj = obj.unwrap()
+ val = "#{olen} <= #{vvarText}.length ? #{ utility 'slice' }.call(#{vvarText}, #{i}"
+ if rest = olen - i - 1
+ ivar = o.scope.freeVariable 'i'
+ val += ", #{ivar} = #{vvarText}.length - #{rest}) : (#{ivar} = #{i}, [])"
+ else
+ val += ") : []"
+ val = new Literal val
+ splat = "#{ivar}++"
+ else
+ name = obj.unwrap().value
+ if obj instanceof Splat
+ obj.error "multiple splats are disallowed in an assignment"
+ if typeof idx is 'number'
+ idx = new Literal splat or idx
+ acc = no
+ else
+ acc = isObject and IDENTIFIER.test idx.unwrap().value or 0
+ val = new Value new Literal(vvarText), [new (if acc then Access else Index) idx]
+ if name? and name in RESERVED
+ obj.error "assignment to a reserved word: #{obj.compile o}"
+ assigns.push new Assign(obj, val, null, param: @param, subpattern: yes).compileToFragments o, LEVEL_LIST
+ assigns.push vvar unless top or @subpattern
+ fragments = @joinFragmentArrays assigns, ', '
+ if o.level < LEVEL_LIST then fragments else @wrapInBraces fragments
+
+
+ ¶
+
+ When compiling a conditional assignment, take care to ensure that the operands are only evaluated once, even though we have to reference them -more than once. | compileConditional: (o) ->
- [left, right] = @variable.cacheReference o |
Disallow conditional assignment of undefined variables. | if not left.properties.length and left.base instanceof Literal and
- left.base.value != "this" and not o.scope.check left.base.value
- throw new Error "the variable \"#{left.base.value}\" can't be assigned with #{@context} because it has not been defined."
- if "?" in @context then o.isExistentialEquals = true
- new Op(@context[...-1], left, new Assign(right, @value, '=') ).compileToFragments o |
Compile the assignment from an array splice literal, using JavaScript's
- | compileSplice: (o) ->
- {range: {from, to, exclusive}} = @variable.properties.pop()
- name = @variable.compile o
- if from
- [fromDecl, fromRef] = @cacheToCodeFragments from.cache o, LEVEL_OP
- else
- fromDecl = fromRef = '0'
- if to
- if from?.isSimpleNumber() and to.isSimpleNumber()
- to = +to.compile(o) - +fromRef
- to += 1 unless exclusive
- else
- to = to.compile(o, LEVEL_ACCESS) + ' - ' + fromRef
- to += ' + 1' unless exclusive
- else
- to = "9e9"
- [valDef, valRef] = @value.cache o, LEVEL_LIST
- answer = [].concat @makeCode("[].splice.apply(#{name}, [#{fromDecl}, #{to}].concat("), valDef, @makeCode(")), "), valRef
- if o.level > LEVEL_TOP then @wrapInBraces answer else answer |
Code | |
A function definition. This is the only node that creates a new Scope. +more than once. + + + + + compileConditional: (o) ->
+ [left, right] = @variable.cacheReference o
+
+
+
+
+ ¶
+
+ Disallow conditional assignment of undefined variables. + + + if not left.properties.length and left.base instanceof Literal and
+ left.base.value != "this" and not o.scope.check left.base.value
+ @variable.error "the variable \"#{left.base.value}\" can't be assigned with #{@context} because it has not been declared before"
+ if "?" in @context then o.isExistentialEquals = true
+ new Op(@context[...-1], left, new Assign(right, @value, '=')).compileToFragments o
+
+
+
+
+ ¶
+
+ Compile the assignment from an array splice literal, using JavaScript's
+ compileSplice: (o) ->
+ {range: {from, to, exclusive}} = @variable.properties.pop()
+ name = @variable.compile o
+ if from
+ [fromDecl, fromRef] = @cacheToCodeFragments from.cache o, LEVEL_OP
+ else
+ fromDecl = fromRef = '0'
+ if to
+ if from?.isSimpleNumber() and to.isSimpleNumber()
+ to = +to.compile(o) - +fromRef
+ to += 1 unless exclusive
+ else
+ to = to.compile(o, LEVEL_ACCESS) + ' - ' + fromRef
+ to += ' + 1' unless exclusive
+ else
+ to = "9e9"
+ [valDef, valRef] = @value.cache o, LEVEL_LIST
+ answer = [].concat @makeCode("[].splice.apply(#{name}, [#{fromDecl}, #{to}].concat("), valDef, @makeCode(")), "), valRef
+ if o.level > LEVEL_TOP then @wrapInBraces answer else answer
+
+
+
+
+ ¶
+
+ Code+ +
+
+
+ ¶
+
+ A function definition. This is the only node that creates a new Scope. When for the purposes of walking the contents of a function body, the Code -has no children -- they're within the inner scope. | exports.Code = class Code extends Base
- constructor: (params, body, tag) ->
- @params = params or []
- @body = body or new Block
- @bound = tag is 'boundfunc'
- @context = '_this' if @bound
-
- children: ['params', 'body']
-
- isStatement: -> !!@ctor
-
- jumps: NO |
Compilation creates a new scope unless explicitly asked to share with the +has no children -- they're within the inner scope. + + + + +exports.Code = class Code extends Base
+ constructor: (params, body, tag) ->
+ @params = params or []
+ @body = body or new Block
+ @bound = tag is 'boundfunc'
+ @context = '_this' if @bound
+
+ children: ['params', 'body']
+
+ isStatement: -> !!@ctor
+
+ jumps: NO
+
+
+ ¶
+
+ Compilation creates a new scope unless explicitly asked to share with the
outer scope. Handles splat parameters in the parameter list by peeking at
the JavaScript | compileNode: (o) ->
- o.scope = new Scope o.scope, @body, this
- o.scope.shared = del(o, 'sharedScope')
- o.indent += TAB
- delete o.bare
- delete o.isExistentialEquals
- params = []
- exprs = []
- for name in @paramNames() # this step must be performed before the others
- unless o.scope.check name then o.scope.parameter name
- for param in @params when param.splat
- for {name: p} in @params
- if p.this then p = p.properties[0].name
- if p.value then o.scope.add p.value, 'var', yes
- splats = new Assign new Value(new Arr(p.asReference o for p in @params)),
- new Value new Literal 'arguments'
- break
- for param in @params
- if param.isComplex()
- val = ref = param.asReference o
- val = new Op '?', ref, param.value if param.value
- exprs.push new Assign new Value(param.name), val, '=', param: yes
- else
- ref = param
- if param.value
- lit = new Literal ref.name.value + ' == null'
- val = new Assign new Value(param.name), param.value, '='
- exprs.push new If lit, val
- params.push ref unless splats
- wasEmpty = @body.isEmpty()
- exprs.unshift splats if splats
- @body.expressions.unshift exprs... if exprs.length
- for p, i in params
- params[i] = p.compileToFragments o
- o.scope.parameter fragmentsToText params[i]
- uniqs = []
- for name in @paramNames()
- throw SyntaxError "multiple parameters named '#{name}'" if name in uniqs
- uniqs.push name
- @body.makeReturn() unless wasEmpty or @noReturn
- if @bound
- if o.scope.parent.method?.bound
- @bound = @context = o.scope.parent.method.context
- else if not @static
- o.scope.parent.assign '_this', 'this'
- idt = o.indent
- code = 'function'
- code += ' ' + @name if @ctor
- code += '('
- answer = [@makeCode(code)]
- for p, i in params
- if i then answer.push @makeCode ", "
- answer.push p...
- answer.push @makeCode ') {'
- answer = answer.concat(@makeCode("\n"), @body.compileWithDeclarations(o), @makeCode("\n#{@tab}")) unless @body.isEmpty()
- answer.push @makeCode '}'
-
- return [@makeCode(@tab), answer...] if @ctor
- if @front or (o.level >= LEVEL_ACCESS) then @wrapInBraces answer else answer |
A list of parameter names, excluding those generated by the compiler. | paramNames: ->
- names = []
- names.push param.names()... for param in @params
- names |
Short-circuit | traverseChildren: (crossScope, func) ->
- super(crossScope, func) if crossScope |
Param | |
A parameter in a function definition. Beyond a typical Javascript parameter, +a closure. + + + + + compileNode: (o) ->
+ o.scope = new Scope o.scope, @body, this
+ o.scope.shared = del(o, 'sharedScope')
+ o.indent += TAB
+ delete o.bare
+ delete o.isExistentialEquals
+ params = []
+ exprs = []
+ @eachParamName (name) -> # this step must be performed before the others
+ unless o.scope.check name then o.scope.parameter name
+ for param in @params when param.splat
+ for {name: p} in @params
+ if p.this then p = p.properties[0].name
+ if p.value then o.scope.add p.value, 'var', yes
+ splats = new Assign new Value(new Arr(p.asReference o for p in @params)),
+ new Value new Literal 'arguments'
+ break
+ for param in @params
+ if param.isComplex()
+ val = ref = param.asReference o
+ val = new Op '?', ref, param.value if param.value
+ exprs.push new Assign new Value(param.name), val, '=', param: yes
+ else
+ ref = param
+ if param.value
+ lit = new Literal ref.name.value + ' == null'
+ val = new Assign new Value(param.name), param.value, '='
+ exprs.push new If lit, val
+ params.push ref unless splats
+ wasEmpty = @body.isEmpty()
+ exprs.unshift splats if splats
+ @body.expressions.unshift exprs... if exprs.length
+ for p, i in params
+ params[i] = p.compileToFragments o
+ o.scope.parameter fragmentsToText params[i]
+ uniqs = []
+ @eachParamName (name, node) ->
+ node.error "multiple parameters named '#{name}'" if name in uniqs
+ uniqs.push name
+ @body.makeReturn() unless wasEmpty or @noReturn
+ if @bound
+ if o.scope.parent.method?.bound
+ @bound = @context = o.scope.parent.method.context
+ else if not @static
+ o.scope.parent.assign '_this', 'this'
+ idt = o.indent
+ code = 'function'
+ code += ' ' + @name if @ctor
+ code += '('
+ answer = [@makeCode(code)]
+ for p, i in params
+ if i then answer.push @makeCode ", "
+ answer.push p...
+ answer.push @makeCode ') {'
+ answer = answer.concat(@makeCode("\n"), @body.compileWithDeclarations(o), @makeCode("\n#{@tab}")) unless @body.isEmpty()
+ answer.push @makeCode '}'
+
+ return [@makeCode(@tab), answer...] if @ctor
+ if @front or (o.level >= LEVEL_ACCESS) then @wrapInBraces answer else answer
+
+ eachParamName: (iterator) ->
+ param.eachName iterator for param in @params
+
+
+
+
+ ¶
+
+ Short-circuit traverseChildren: (crossScope, func) ->
+ super(crossScope, func) if crossScope
+
+
+
+
+ ¶
+
+ Param+ +
+
+
+ ¶
+
+ A parameter in a function definition. Beyond a typical Javascript parameter, these parameters can also attach themselves to the context of the function, -as well as be a splat, gathering up a group of parameters into an array. | exports.Param = class Param extends Base
- constructor: (@name, @value, @splat) ->
- if (name = @name.unwrapAll().value) in STRICT_PROSCRIBED
- throw SyntaxError "parameter name \"#{name}\" is not allowed"
-
- children: ['name', 'value']
-
- compileToFragments: (o) ->
- @name.compileToFragments o, LEVEL_LIST
-
- asReference: (o) ->
- return @reference if @reference
- node = @name
- if node.this
- node = node.properties[0].name
- if node.value.reserved
- node = new Literal o.scope.freeVariable node.value
- else if node.isComplex()
- node = new Literal o.scope.freeVariable 'arg'
- node = new Value node
- node = new Splat node if @splat
- @reference = node
-
- isComplex: ->
- @name.isComplex() |
Finds the name or names of a | names: (name = @name)->
- atParam = (obj) ->
- {value} = obj.properties[0].name
- return if value.reserved then [] else [value] |
exports.Param = class Param extends Base
+ constructor: (@name, @value, @splat) ->
+ if (name = @name.unwrapAll().value) in STRICT_PROSCRIBED
+ @name.error "parameter name \"#{name}\" is not allowed"
+
+ children: ['name', 'value']
+
+ compileToFragments: (o) ->
+ @name.compileToFragments o, LEVEL_LIST
+
+ asReference: (o) ->
+ return @reference if @reference
+ node = @name
+ if node.this
+ node = node.properties[0].name
+ if node.value.reserved
+ node = new Literal o.scope.freeVariable node.value
+ else if node.isComplex()
+ node = new Literal o.scope.freeVariable 'arg'
+ node = new Value node
+ node = new Splat node if @splat
+ @reference = node
+
+ isComplex: ->
+ @name.isComplex() | return [name.value] if name instanceof Literal |
return iterator name.value, name if name instanceof Literal
+
+
+ ¶
+
+
| return atParam(name) if name instanceof Value
- names = []
- for obj in name.objects |
return atParam name if name instanceof Value
+ for obj in name.objects
+
+
+ ¶
+
+
| if obj instanceof Assign
- names.push @names(obj.value.unwrap())... |
if obj instanceof Assign
+ @eachName iterator, obj.value.unwrap()
+
+
+ ¶
+
+
| else if obj instanceof Splat
- names.push obj.name.unwrap().value
- else if obj instanceof Value |
else if obj instanceof Splat
+ node = obj.name.unwrap()
+ iterator node.value, node
+ else if obj instanceof Value
+
+
+ ¶
+
+
| if obj.isArray() or obj.isObject()
- names.push @names(obj.base)... |
if obj.isArray() or obj.isObject()
+ @eachName iterator, obj.base
+
+
+ ¶
+
+
| else if obj.this
- names.push atParam(obj)... |
else if obj.this
+ atParam obj
+
+
+ ¶
+
+
| else names.push obj.base.value
- else
- throw SyntaxError "illegal parameter #{obj.compile()}"
- names |
Splat | |
A splat, either as a parameter to a function, an argument to a call, -or as part of a destructuring assignment. | exports.Splat = class Splat extends Base
-
- children: ['name']
-
- isAssignable: YES
-
- constructor: (name) ->
- @name = if name.compile then name else new Literal name
-
- assigns: (name) ->
- @name.assigns name
-
- compileToFragments: (o) ->
- @name.compileToFragments o
-
- unwrap: -> @name |
Utility function that converts an arbitrary number of elements, mixed with -splats, to a proper array. | @compileSplattedArray: (o, list, apply) ->
- index = -1
- continue while (node = list[++index]) and node not instanceof Splat
- return [] if index >= list.length
- if list.length is 1
- node = list[0]
- fragments = node.compileToFragments o, LEVEL_LIST
- return fragments if apply
- return [].concat node.makeCode("#{ utility 'slice' }.call("), fragments, node.makeCode(")")
- args = list[index..]
- for node, i in args
- compiledNode = node.compileToFragments o, LEVEL_LIST
- args[i] = if node instanceof Splat
- then [].concat node.makeCode("#{ utility 'slice' }.call("), compiledNode, node.makeCode(")")
- else [].concat node.makeCode("["), compiledNode, node.makeCode("]")
- if index is 0
- node = list[0]
- concatPart = (node.joinFragmentArrays args[1..], ', ')
- return args[0].concat node.makeCode(".concat("), concatPart, node.makeCode(")")
- base = (node.compileToFragments o, LEVEL_LIST for node in list[...index])
- base = list[0].joinFragmentArrays base, ', '
- concatPart = list[index].joinFragmentArrays args, ', '
- [].concat list[0].makeCode("["), base, list[index].makeCode("].concat("), concatPart, (last list).makeCode(")") |
While | |
A while loop, the only sort of low-level loop exposed by CoffeeScript. From + + + + + else iterator obj.base.value, obj.base
+ else
+ obj.error "illegal parameter #{obj.compile()}"
+ return
+
+
+
+
+ ¶
+
+ Splat+ +
+
+
+
+
+ ¶
+
+ A splat, either as a parameter to a function, an argument to a call, +or as part of a destructuring assignment. + + +exports.Splat = class Splat extends Base
+
+ children: ['name']
+
+ isAssignable: YES
+
+ constructor: (name) ->
+ @name = if name.compile then name else new Literal name
+
+ assigns: (name) ->
+ @name.assigns name
+
+ compileToFragments: (o) ->
+ @name.compileToFragments o
+
+ unwrap: -> @name
+
+
+
+
+ ¶
+
+ Utility function that converts an arbitrary number of elements, mixed with +splats, to a proper array. + + + @compileSplattedArray: (o, list, apply) ->
+ index = -1
+ continue while (node = list[++index]) and node not instanceof Splat
+ return [] if index >= list.length
+ if list.length is 1
+ node = list[0]
+ fragments = node.compileToFragments o, LEVEL_LIST
+ return fragments if apply
+ return [].concat node.makeCode("#{ utility 'slice' }.call("), fragments, node.makeCode(")")
+ args = list[index..]
+ for node, i in args
+ compiledNode = node.compileToFragments o, LEVEL_LIST
+ args[i] = if node instanceof Splat
+ then [].concat node.makeCode("#{ utility 'slice' }.call("), compiledNode, node.makeCode(")")
+ else [].concat node.makeCode("["), compiledNode, node.makeCode("]")
+ if index is 0
+ node = list[0]
+ concatPart = (node.joinFragmentArrays args[1..], ', ')
+ return args[0].concat node.makeCode(".concat("), concatPart, node.makeCode(")")
+ base = (node.compileToFragments o, LEVEL_LIST for node in list[...index])
+ base = list[0].joinFragmentArrays base, ', '
+ concatPart = list[index].joinFragmentArrays args, ', '
+ [].concat list[0].makeCode("["), base, list[index].makeCode("].concat("), concatPart, (last list).makeCode(")")
+
+
+
+
+ ¶
+
+ While+ +
+
+
+ ¶
+
+ A while loop, the only sort of low-level loop exposed by CoffeeScript. From it, all other loops can be manufactured. Useful in cases where you need more -flexibility or more speed than a comprehension can provide. | exports.While = class While extends Base
- constructor: (condition, options) ->
- @condition = if options?.invert then condition.invert() else condition
- @guard = options?.guard
-
- children: ['condition', 'guard', 'body']
-
- isStatement: YES
-
- makeReturn: (res) ->
- if res
- super
- else
- @returns = not @jumps loop: yes
- this
-
- addBody: (@body) ->
- this
-
- jumps: ->
- {expressions} = @body
- return no unless expressions.length
- for node in expressions
- return node if node.jumps loop: yes
- no |
The main difference from a JavaScript while is that the CoffeeScript +flexibility or more speed than a comprehension can provide. + + + + +exports.While = class While extends Base
+ constructor: (condition, options) ->
+ @condition = if options?.invert then condition.invert() else condition
+ @guard = options?.guard
+
+ children: ['condition', 'guard', 'body']
+
+ isStatement: YES
+
+ makeReturn: (res) ->
+ if res
+ super
+ else
+ @returns = not @jumps loop: yes
+ this
+
+ addBody: (@body) ->
+ this
+
+ jumps: ->
+ {expressions} = @body
+ return no unless expressions.length
+ for node in expressions
+ return node if node.jumps loop: yes
+ no
+
+
+ ¶
+
+ The main difference from a JavaScript while is that the CoffeeScript while can be used as a part of a larger expression -- while loops may -return an array containing the computed result of each iteration. | compileNode: (o) ->
- o.indent += TAB
- set = ''
- {body} = this
- if body.isEmpty()
- body = ''
- else
- if @returns
- body.makeReturn rvar = o.scope.freeVariable 'results'
- set = "#{@tab}#{rvar} = [];\n"
- if @guard
- if body.expressions.length > 1
- body.expressions.unshift new If (new Parens @guard).invert(), new Literal "continue"
- else
- body = Block.wrap [new If @guard, body] if @guard
- body = [].concat @makeCode("\n"), (body.compileToFragments o, LEVEL_TOP), @makeCode("\n#{@tab}")
- answer = [].concat @makeCode(set + @tab + "while ("), @condition.compileToFragments(o, LEVEL_PAREN),
- @makeCode(") {"), body, @makeCode("}")
- if @returns
- answer.push @makeCode "\n#{@tab}return #{rvar};"
- answer |
Op | |
Simple Arithmetic and logical operations. Performs some conversion from -CoffeeScript operations into their JavaScript equivalents. | exports.Op = class Op extends Base
- constructor: (op, first, second, flip ) ->
- return new In first, second if op is 'in'
- if op is 'do'
- return @generateDo first
- if op is 'new'
- return first.newInstance() if first instanceof Call and not first.do and not first.isNew
- first = new Parens first if first instanceof Code and first.bound or first.do
- @operator = CONVERSIONS[op] or op
- @first = first
- @second = second
- @flip = !!flip
- return this |
The map of conversions from CoffeeScript to JavaScript symbols. | CONVERSIONS =
- '==': '==='
- '!=': '!=='
- 'of': 'in' |
The map of invertible operators. | INVERSIONS =
- '!==': '==='
- '===': '!=='
-
- children: ['first', 'second']
-
- isSimpleNumber: NO
-
- isUnary: ->
- not @second
-
- isComplex: ->
- not (@isUnary() and (@operator in ['+', '-'])) or @first.isComplex() |
Am I capable of -Python-style comparison chaining? | isChainable: ->
- @operator in ['<', '>', '>=', '<=', '===', '!==']
-
- invert: ->
- if @isChainable() and @first.isChainable()
- allInvertable = yes
- curr = this
- while curr and curr.operator
- allInvertable and= (curr.operator of INVERSIONS)
- curr = curr.first
- return new Parens(this).invert() unless allInvertable
- curr = this
- while curr and curr.operator
- curr.invert = !curr.invert
- curr.operator = INVERSIONS[curr.operator]
- curr = curr.first
- this
- else if op = INVERSIONS[@operator]
- @operator = op
- if @first.unwrap() instanceof Op
- @first.invert()
- this
- else if @second
- new Parens(this).invert()
- else if @operator is '!' and (fst = @first.unwrap()) instanceof Op and
- fst.operator in ['!', 'in', 'instanceof']
- fst
- else
- new Op '!', this
-
- unfoldSoak: (o) ->
- @operator in ['++', '--', 'delete'] and unfoldSoak o, this, 'first'
-
- generateDo: (exp) ->
- passedParams = []
- func = if exp instanceof Assign and (ref = exp.value.unwrap()) instanceof Code
- ref
- else
- exp
- for param in func.params or []
- if param.value
- passedParams.push param.value
- delete param.value
- else
- passedParams.push param
- call = new Call exp, passedParams
- call.do = yes
- call
-
- compileNode: (o) ->
- isChain = @isChainable() and @first.isChainable() |
In chains, there's no need to wrap bare obj literals in parens, -as the chained expression is wrapped. | @first.front = @front unless isChain
- if @operator is 'delete' and o.scope.check(@first.unwrapAll().value)
- throw SyntaxError 'delete operand may not be argument or var'
- if @operator in ['--', '++'] and @first.unwrapAll().value in STRICT_PROSCRIBED
- throw SyntaxError 'prefix increment/decrement may not have eval or arguments operand'
- return @compileUnary o if @isUnary()
- return @compileChain o if isChain
- return @compileExistence o if @operator is '?'
- answer = [].concat @first.compileToFragments(o, LEVEL_OP), @makeCode(' ' + @operator + ' '),
- @second.compileToFragments(o, LEVEL_OP)
- if o.level <= LEVEL_OP then answer else @wrapInBraces answer |
Mimic Python's chained comparisons when multiple comparison operators are -used sequentially. For example: - - | compileChain: (o) ->
- [@first.second, shared] = @first.second.cache o
- fst = @first.compileToFragments o, LEVEL_OP
- fragments = fst.concat @makeCode(" #{if @invert then '&&' else '||'} "),
- (shared.compileToFragments o), @makeCode(" #{@operator} "), (@second.compileToFragments o, LEVEL_OP)
- @wrapInBraces fragments
-
- compileExistence: (o) ->
- if @first.isComplex()
- ref = new Literal o.scope.freeVariable 'ref'
- fst = new Parens new Assign ref, @first
- else
- fst = @first
- ref = fst
- new If(new Existence(fst), ref, type: 'if').addElse(@second).compileToFragments o |
Compile a unary Op. | compileUnary: (o) ->
- parts = []
- op = @operator
- parts.push [@makeCode op]
- if op is '!' and @first instanceof Existence
- @first.negated = not @first.negated
- return @first.compileToFragments o
- if o.level >= LEVEL_ACCESS
- return (new Parens this).compileToFragments o
- plusMinus = op in ['+', '-']
- parts.push [@makeCode(' ')] if op in ['new', 'typeof', 'delete'] or
- plusMinus and @first instanceof Op and @first.operator is op
- if (plusMinus && @first instanceof Op) or (op is 'new' and @first.isStatement o)
- @first = new Parens @first
- parts.push @first.compileToFragments o, LEVEL_OP
- parts.reverse() if @flip
- @joinFragmentArrays parts, ''
-
- toString: (idt) ->
- super idt, @constructor.name + ' ' + @operator |
In | exports.In = class In extends Base
- constructor: (@object, @array) ->
-
- children: ['object', 'array']
-
- invert: NEGATE
-
- compileNode: (o) ->
- if @array instanceof Value and @array.isArray()
- for obj in @array.base.objects when obj instanceof Splat
- hasSplat = yes
- break |
| return @compileOrTest o unless hasSplat
- @compileLoopTest o
-
- compileOrTest: (o) ->
- return [@makeCode("#{!!@negated}")] if @array.base.objects.length is 0
- [sub, ref] = @object.cache o, LEVEL_OP
- [cmp, cnj] = if @negated then [' !== ', ' && '] else [' === ', ' || ']
- tests = []
- for item, i in @array.base.objects
- if i then tests.push @makeCode cnj
- tests = tests.concat (if i then ref else sub), @makeCode(cmp), item.compileToFragments(o, LEVEL_ACCESS)
- if o.level < LEVEL_OP then tests else @wrapInBraces tests
-
- compileLoopTest: (o) ->
- [sub, ref] = @object.cache o, LEVEL_LIST
- fragments = [].concat @makeCode(utility('indexOf') + ".call("), @array.compileToFragments(o, LEVEL_LIST),
- @makeCode(", "), ref, @makeCode(") " + if @negated then '< 0' else '>= 0')
- return fragments if (fragmentsToText sub) is (fragmentsToText ref)
- fragments = sub.concat @makeCode(', '), fragments
- if o.level < LEVEL_LIST then fragments else @wrapInBraces fragments
-
- toString: (idt) ->
- super idt, @constructor.name + if @negated then '!' else '' |
Try | |
A classic try/catch/finally block. | exports.Try = class Try extends Base
- constructor: (@attempt, @error, @recovery, @ensure) ->
-
- children: ['attempt', 'recovery', 'ensure']
-
- isStatement: YES
-
- jumps: (o) -> @attempt.jumps(o) or @recovery?.jumps(o)
-
- makeReturn: (res) ->
- @attempt = @attempt .makeReturn res if @attempt
- @recovery = @recovery.makeReturn res if @recovery
- this |
Compilation is more or less as you would expect -- the finally clause -is optional, the catch is not. | compileNode: (o) ->
- o.indent += TAB
- tryPart = @attempt.compileToFragments o, LEVEL_TOP
-
- catchPart = if @recovery
- if @error.isObject?()
- placeholder = new Literal '_error'
- @recovery.unshift new Assign @error, placeholder
- @error = placeholder
- if @error.value in STRICT_PROSCRIBED
- throw SyntaxError "catch variable may not be \"#{@error.value}\""
- o.scope.add @error.value, 'param' unless o.scope.check @error.value
- [].concat @makeCode(" catch ("), @error.compileToFragments(o), @makeCode(") {\n"),
- @recovery.compileToFragments(o, LEVEL_TOP), @makeCode("\n#{@tab}}")
- else unless @ensure or @recovery
- [@makeCode(' catch (_error) {}')]
- else
- []
-
- ensurePart = if @ensure then ([].concat @makeCode(" finally {\n"), (@ensure.compileToFragments o, LEVEL_TOP), @makeCode("\n#{@tab}}")) else []
-
- [].concat @makeCode("#{@tab}try {\n"),
- tryPart,
- @makeCode("\n#{@tab}}"), catchPart, ensurePart |
Throw | |
Simple node to throw an exception. | exports.Throw = class Throw extends Base
- constructor: (@expression) ->
-
- children: ['expression']
-
- isStatement: YES
- jumps: NO |
A Throw is already a return, of sorts... | makeReturn: THIS
-
- compileNode: (o) ->
- [].concat @makeCode(@tab + "throw "), (@expression.compileToFragments o), @makeCode(";") |
Existence | |
Checks a variable for existence -- not null and not undefined. This is +return an array containing the computed result of each iteration. + + + + + compileNode: (o) ->
+ o.indent += TAB
+ set = ''
+ {body} = this
+ if body.isEmpty()
+ body = @makeCode ''
+ else
+ if @returns
+ body.makeReturn rvar = o.scope.freeVariable 'results'
+ set = "#{@tab}#{rvar} = [];\n"
+ if @guard
+ if body.expressions.length > 1
+ body.expressions.unshift new If (new Parens @guard).invert(), new Literal "continue"
+ else
+ body = Block.wrap [new If @guard, body] if @guard
+ body = [].concat @makeCode("\n"), (body.compileToFragments o, LEVEL_TOP), @makeCode("\n#{@tab}")
+ answer = [].concat @makeCode(set + @tab + "while ("), @condition.compileToFragments(o, LEVEL_PAREN),
+ @makeCode(") {"), body, @makeCode("}")
+ if @returns
+ answer.push @makeCode "\n#{@tab}return #{rvar};"
+ answer
+
+
+
+
+ ¶
+
+ Op+ +
+
+
+
+
+ ¶
+
+ Simple Arithmetic and logical operations. Performs some conversion from +CoffeeScript operations into their JavaScript equivalents. + + +exports.Op = class Op extends Base
+ constructor: (op, first, second, flip ) ->
+ return new In first, second if op is 'in'
+ if op is 'do'
+ return @generateDo first
+ if op is 'new'
+ return first.newInstance() if first instanceof Call and not first.do and not first.isNew
+ first = new Parens first if first instanceof Code and first.bound or first.do
+ @operator = CONVERSIONS[op] or op
+ @first = first
+ @second = second
+ @flip = !!flip
+ return this
+
+
+
+
+ ¶
+
+ The map of conversions from CoffeeScript to JavaScript symbols. + + + CONVERSIONS =
+ '==': '==='
+ '!=': '!=='
+ 'of': 'in'
+
+
+
+
+ ¶
+
+ The map of invertible operators. + + + INVERSIONS =
+ '!==': '==='
+ '===': '!=='
+
+ children: ['first', 'second']
+
+ isSimpleNumber: NO
+
+ isUnary: ->
+ not @second
+
+ isComplex: ->
+ not (@isUnary() and @operator in ['+', '-']) or @first.isComplex()
+
+
+
+
+ ¶
+
+ Am I capable of +Python-style comparison chaining? + + + isChainable: ->
+ @operator in ['<', '>', '>=', '<=', '===', '!==']
+
+ invert: ->
+ if @isChainable() and @first.isChainable()
+ allInvertable = yes
+ curr = this
+ while curr and curr.operator
+ allInvertable and= (curr.operator of INVERSIONS)
+ curr = curr.first
+ return new Parens(this).invert() unless allInvertable
+ curr = this
+ while curr and curr.operator
+ curr.invert = !curr.invert
+ curr.operator = INVERSIONS[curr.operator]
+ curr = curr.first
+ this
+ else if op = INVERSIONS[@operator]
+ @operator = op
+ if @first.unwrap() instanceof Op
+ @first.invert()
+ this
+ else if @second
+ new Parens(this).invert()
+ else if @operator is '!' and (fst = @first.unwrap()) instanceof Op and
+ fst.operator in ['!', 'in', 'instanceof']
+ fst
+ else
+ new Op '!', this
+
+ unfoldSoak: (o) ->
+ @operator in ['++', '--', 'delete'] and unfoldSoak o, this, 'first'
+
+ generateDo: (exp) ->
+ passedParams = []
+ func = if exp instanceof Assign and (ref = exp.value.unwrap()) instanceof Code
+ ref
+ else
+ exp
+ for param in func.params or []
+ if param.value
+ passedParams.push param.value
+ delete param.value
+ else
+ passedParams.push param
+ call = new Call exp, passedParams
+ call.do = yes
+ call
+
+ compileNode: (o) ->
+ isChain = @isChainable() and @first.isChainable()
+
+
+
+
+ ¶
+
+ In chains, there's no need to wrap bare obj literals in parens, +as the chained expression is wrapped. + + + @first.front = @front unless isChain
+ if @operator is 'delete' and o.scope.check(@first.unwrapAll().value)
+ @error 'delete operand may not be argument or var'
+ if @operator in ['--', '++'] and @first.unwrapAll().value in STRICT_PROSCRIBED
+ @error "cannot increment/decrement \"#{@first.unwrapAll().value}\""
+ return @compileUnary o if @isUnary()
+ return @compileChain o if isChain
+ return @compileExistence o if @operator is '?'
+ answer = [].concat @first.compileToFragments(o, LEVEL_OP), @makeCode(' ' + @operator + ' '),
+ @second.compileToFragments(o, LEVEL_OP)
+ if o.level <= LEVEL_OP then answer else @wrapInBraces answer
+
+
+
+
+ ¶
+
+ Mimic Python's chained comparisons when multiple comparison operators are +used sequentially. For example: + + +
+
+ compileChain: (o) ->
+ [@first.second, shared] = @first.second.cache o
+ fst = @first.compileToFragments o, LEVEL_OP
+ fragments = fst.concat @makeCode(" #{if @invert then '&&' else '||'} "),
+ (shared.compileToFragments o), @makeCode(" #{@operator} "), (@second.compileToFragments o, LEVEL_OP)
+ @wrapInBraces fragments
+
+
+
+
+ ¶
+
+ Keep reference to the left expression, unless this an existential assignment + + + compileExistence: (o) ->
+ if !o.isExistentialEquals and @first.isComplex()
+ ref = new Literal o.scope.freeVariable 'ref'
+ fst = new Parens new Assign ref, @first
+ else
+ fst = @first
+ ref = fst
+ new If(new Existence(fst), ref, type: 'if').addElse(@second).compileToFragments o
+
+
+
+
+ ¶
+
+ Compile a unary Op. + + + compileUnary: (o) ->
+ parts = []
+ op = @operator
+ parts.push [@makeCode op]
+ if op is '!' and @first instanceof Existence
+ @first.negated = not @first.negated
+ return @first.compileToFragments o
+ if o.level >= LEVEL_ACCESS
+ return (new Parens this).compileToFragments o
+ plusMinus = op in ['+', '-']
+ parts.push [@makeCode(' ')] if op in ['new', 'typeof', 'delete'] or
+ plusMinus and @first instanceof Op and @first.operator is op
+ if (plusMinus and @first instanceof Op) or (op is 'new' and @first.isStatement o)
+ @first = new Parens @first
+ parts.push @first.compileToFragments o, LEVEL_OP
+ parts.reverse() if @flip
+ @joinFragmentArrays parts, ''
+
+ toString: (idt) ->
+ super idt, @constructor.name + ' ' + @operator
+
+
+
+
+ ¶
+
+ In+ +exports.In = class In extends Base
+ constructor: (@object, @array) ->
+
+ children: ['object', 'array']
+
+ invert: NEGATE
+
+ compileNode: (o) ->
+ if @array instanceof Value and @array.isArray()
+ for obj in @array.base.objects when obj instanceof Splat
+ hasSplat = yes
+ break
+
+
+
+
+ ¶
+
+
return @compileOrTest o unless hasSplat
+ @compileLoopTest o
+
+ compileOrTest: (o) ->
+ return [@makeCode("#{!!@negated}")] if @array.base.objects.length is 0
+ [sub, ref] = @object.cache o, LEVEL_OP
+ [cmp, cnj] = if @negated then [' !== ', ' && '] else [' === ', ' || ']
+ tests = []
+ for item, i in @array.base.objects
+ if i then tests.push @makeCode cnj
+ tests = tests.concat (if i then ref else sub), @makeCode(cmp), item.compileToFragments(o, LEVEL_ACCESS)
+ if o.level < LEVEL_OP then tests else @wrapInBraces tests
+
+ compileLoopTest: (o) ->
+ [sub, ref] = @object.cache o, LEVEL_LIST
+ fragments = [].concat @makeCode(utility('indexOf') + ".call("), @array.compileToFragments(o, LEVEL_LIST),
+ @makeCode(", "), ref, @makeCode(") " + if @negated then '< 0' else '>= 0')
+ return fragments if fragmentsToText(sub) is fragmentsToText(ref)
+ fragments = sub.concat @makeCode(', '), fragments
+ if o.level < LEVEL_LIST then fragments else @wrapInBraces fragments
+
+ toString: (idt) ->
+ super idt, @constructor.name + if @negated then '!' else ''
+
+
+
+
+ ¶
+
+ Try+ +
+
+
+
+
+ ¶
+
+ A classic try/catch/finally block. + + +exports.Try = class Try extends Base
+ constructor: (@attempt, @errorVariable, @recovery, @ensure) ->
+
+ children: ['attempt', 'recovery', 'ensure']
+
+ isStatement: YES
+
+ jumps: (o) -> @attempt.jumps(o) or @recovery?.jumps(o)
+
+ makeReturn: (res) ->
+ @attempt = @attempt .makeReturn res if @attempt
+ @recovery = @recovery.makeReturn res if @recovery
+ this
+
+
+
+
+ ¶
+
+ Compilation is more or less as you would expect -- the finally clause +is optional, the catch is not. + + + compileNode: (o) ->
+ o.indent += TAB
+ tryPart = @attempt.compileToFragments o, LEVEL_TOP
+
+ catchPart = if @recovery
+ placeholder = new Literal '_error'
+ @recovery.unshift new Assign @errorVariable, placeholder if @errorVariable
+ [].concat @makeCode(" catch ("), placeholder.compileToFragments(o), @makeCode(") {\n"),
+ @recovery.compileToFragments(o, LEVEL_TOP), @makeCode("\n#{@tab}}")
+ else unless @ensure or @recovery
+ [@makeCode(' catch (_error) {}')]
+ else
+ []
+
+ ensurePart = if @ensure then ([].concat @makeCode(" finally {\n"), @ensure.compileToFragments(o, LEVEL_TOP),
+ @makeCode("\n#{@tab}}")) else []
+
+ [].concat @makeCode("#{@tab}try {\n"),
+ tryPart,
+ @makeCode("\n#{@tab}}"), catchPart, ensurePart
+
+
+
+
+ ¶
+
+ Throw+ +
+
+
+
+
+ ¶
+
+ Simple node to throw an exception. + + +exports.Throw = class Throw extends Base
+ constructor: (@expression) ->
+
+ children: ['expression']
+
+ isStatement: YES
+ jumps: NO
+
+
+
+
+ ¶
+
+ A Throw is already a return, of sorts... + + + makeReturn: THIS
+
+ compileNode: (o) ->
+ [].concat @makeCode(@tab + "throw "), @expression.compileToFragments(o), @makeCode(";")
+
+
+
+
+ ¶
+
+ Existence+ +
+
+
+ ¶
+
+ Checks a variable for existence -- not null and not undefined. This is
similar to | exports.Existence = class Existence extends Base
- constructor: (@expression) ->
-
- children: ['expression']
-
- invert: NEGATE
-
- compileNode: (o) ->
- @expression.front = @front
- code = @expression.compile o, LEVEL_OP
- if IDENTIFIER.test(code) and not o.scope.check code
- [cmp, cnj] = if @negated then ['===', '||'] else ['!==', '&&']
- code = "typeof #{code} #{cmp} \"undefined\" #{cnj} #{code} #{cmp} null"
- else |
do not use strict equality here; it will break existing code | code = "#{code} #{if @negated then '==' else '!='} null"
- [@makeCode(if o.level <= LEVEL_COND then code else "(#{code})")] |
Parens | |
An extra set of parentheses, specified explicitly in the source. At one time +table. + + + + +exports.Existence = class Existence extends Base
+ constructor: (@expression) ->
+
+ children: ['expression']
+
+ invert: NEGATE
+
+ compileNode: (o) ->
+ @expression.front = @front
+ code = @expression.compile o, LEVEL_OP
+ if IDENTIFIER.test(code) and not o.scope.check code
+ [cmp, cnj] = if @negated then ['===', '||'] else ['!==', '&&']
+ code = "typeof #{code} #{cmp} \"undefined\" #{cnj} #{code} #{cmp} null"
+ else
+
+
+
+
+ ¶
+
+ do not use strict equality here; it will break existing code + + + code = "#{code} #{if @negated then '==' else '!='} null"
+ [@makeCode(if o.level <= LEVEL_COND then code else "(#{code})")]
+
+
+
+
+ ¶
+
+ Parens+ +
+
+
+ ¶
+
+ An extra set of parentheses, specified explicitly in the source. At one time we tried to clean up the results by detecting and removing redundant -parentheses, but no longer -- you can put in as many as you please. - -Parentheses are a good way to force any statement to become an expression. | exports.Parens = class Parens extends Base
- constructor: (@body) ->
-
- children: ['body']
-
- unwrap : -> @body
- isComplex : -> @body.isComplex()
-
- compileNode: (o) ->
- expr = @body.unwrap()
- if expr instanceof Value and expr.isAtomic()
- expr.front = @front
- return expr.compileToFragments o
- fragments = expr.compileToFragments o, LEVEL_PAREN
- bare = o.level < LEVEL_OP and (expr instanceof Op or expr instanceof Call or
- (expr instanceof For and expr.returns))
- if bare then fragments else @wrapInBraces fragments |
For | |
CoffeeScript's replacement for the for loop is our array and object +parentheses, but no longer -- you can put in as many as you please. + + +Parentheses are a good way to force any statement to become an expression. + + + + +exports.Parens = class Parens extends Base
+ constructor: (@body) ->
+
+ children: ['body']
+
+ unwrap : -> @body
+ isComplex : -> @body.isComplex()
+
+ compileNode: (o) ->
+ expr = @body.unwrap()
+ if expr instanceof Value and expr.isAtomic()
+ expr.front = @front
+ return expr.compileToFragments o
+ fragments = expr.compileToFragments o, LEVEL_PAREN
+ bare = o.level < LEVEL_OP and (expr instanceof Op or expr instanceof Call or
+ (expr instanceof For and expr.returns))
+ if bare then fragments else @wrapInBraces fragments
+
+
+
+
+ ¶
+
+ For+ +
+
+
+ ¶
+
+ CoffeeScript's replacement for the for loop is our array and object comprehensions, that compile into for loops here. They also act as an -expression, able to return the result of each filtered iteration. +expression, able to return the result of each filtered iteration. +Unlike Python array comprehensions, they can be multi-line, and you can pass the current index of the loop as a second parameter. Unlike Ruby blocks, -you can map and filter in a single pass. | exports.For = class For extends While
- constructor: (body, source) ->
- {@source, @guard, @step, @name, @index} = source
- @body = Block.wrap [body]
- @own = !!source.own
- @object = !!source.object
- [@name, @index] = [@index, @name] if @object
- throw SyntaxError 'index cannot be a pattern matching expression' if @index instanceof Value
- @range = @source instanceof Value and @source.base instanceof Range and not @source.properties.length
- @pattern = @name instanceof Value
- throw SyntaxError 'indexes do not apply to range loops' if @range and @index
- throw SyntaxError 'cannot pattern match over range loops' if @range and @pattern
- @returns = false
-
- children: ['body', 'source', 'guard', 'step'] |
Welcome to the hairiest method in all of CoffeeScript. Handles the inner +you can map and filter in a single pass. + + + + +exports.For = class For extends While
+ constructor: (body, source) ->
+ {@source, @guard, @step, @name, @index} = source
+ @body = Block.wrap [body]
+ @own = !!source.own
+ @object = !!source.object
+ [@name, @index] = [@index, @name] if @object
+ @index.error 'index cannot be a pattern matching expression' if @index instanceof Value
+ @range = @source instanceof Value and @source.base instanceof Range and not @source.properties.length
+ @pattern = @name instanceof Value
+ @index.error 'indexes do not apply to range loops' if @range and @index
+ @name.error 'cannot pattern match over range loops' if @range and @pattern
+ @index.error 'cannot use own with for-in' if @own and not @object
+ @returns = false
+
+ children: ['body', 'source', 'guard', 'step']
+
+
+ ¶
+
+ Welcome to the hairiest method in all of CoffeeScript. Handles the inner loop, filtering, stepping, and result saving for array, object, and range comprehensions. Some of the generated code can be shared in common, and -some cannot. | compileNode: (o) ->
- body = Block.wrap [@body]
- lastJumps = last(body.expressions)?.jumps()
- @returns = no if lastJumps and lastJumps instanceof Return
- source = if @range then @source.base else @source
- scope = o.scope
- name = @name and (@name.compile o, LEVEL_LIST)
- index = @index and (@index.compile o, LEVEL_LIST)
- scope.find(name) if name and not @pattern
- scope.find(index) if index
- rvar = scope.freeVariable 'results' if @returns
- ivar = (@object and index) or scope.freeVariable 'i'
- kvar = (@range and name) or index or ivar
- kvarAssign = if kvar isnt ivar then "#{kvar} = " else ""
- if @step and not @range
- [step, stepVar] = @cacheToCodeFragments @step.cache o, LEVEL_LIST
- stepNum = stepVar.match SIMPLENUM
- name = ivar if @pattern
- varPart = ''
- guardPart = ''
- defPart = ''
- idt1 = @tab + TAB
- if @range
- forPartFragments = source.compileToFragments merge(o, {index: ivar, name, @step})
- else
- svar = @source.compile o, LEVEL_LIST
- if (name or @own) and not IDENTIFIER.test svar
- defPart += "#{@tab}#{ref = scope.freeVariable 'ref'} = #{svar};\n"
- svar = ref
- if name and not @pattern
- namePart = "#{name} = #{svar}[#{kvar}]"
- if not @object
- defPart += "#{@tab}#{step};\n" if step isnt stepVar
- lvar = scope.freeVariable 'len' unless @step and stepNum and down = (+stepNum < 0)
- declare = "#{kvarAssign}#{ivar} = 0, #{lvar} = #{svar}.length"
- declareDown = "#{kvarAssign}#{ivar} = #{svar}.length - 1"
- compare = "#{ivar} < #{lvar}"
- compareDown = "#{ivar} >= 0"
- if @step
- if stepNum
- if down
- compare = compareDown
- declare = declareDown
- else
- compare = "#{stepVar} > 0 ? #{compare} : #{compareDown}"
- declare = "(#{stepVar} > 0 ? (#{declare}) : #{declareDown})"
- increment = "#{ivar} += #{stepVar}"
- else
- increment = "#{if kvar isnt ivar then "++#{ivar}" else "#{ivar}++"}"
- forPartFragments = [@makeCode("#{declare}; #{compare}; #{kvarAssign}#{increment}")]
- if @returns
- resultPart = "#{@tab}#{rvar} = [];\n"
- returnResult = "\n#{@tab}return #{rvar};"
- body.makeReturn rvar
- if @guard
- if body.expressions.length > 1
- body.expressions.unshift new If (new Parens @guard).invert(), new Literal "continue"
- else
- body = Block.wrap [new If @guard, body] if @guard
- if @pattern
- body.expressions.unshift new Assign @name, new Literal "#{svar}[#{kvar}]"
- defPartFragments = [].concat @makeCode(defPart), @pluckDirectCall(o, body)
- varPart = "\n#{idt1}#{namePart};" if namePart
- if @object
- forPartFragments = [@makeCode("#{kvar} in #{svar}")]
- guardPart = "\n#{idt1}if (!#{utility 'hasProp'}.call(#{svar}, #{kvar})) continue;" if @own
- bodyFragments = body.compileToFragments merge(o, indent: idt1), LEVEL_TOP
- if bodyFragments and (bodyFragments.length > 0)
- bodyFragments = [].concat @makeCode("\n"), bodyFragments, @makeCode("\n")
- [].concat defPartFragments, @makeCode("#{resultPart or ''}#{@tab}for ("),
- forPartFragments, @makeCode(") {#{guardPart}#{varPart}"), bodyFragments,
- @makeCode("#{@tab}}#{returnResult or ''}")
-
- pluckDirectCall: (o, body) ->
- defs = []
- for expr, idx in body.expressions
- expr = expr.unwrapAll()
- continue unless expr instanceof Call
- val = expr.variable.unwrapAll()
- continue unless (val instanceof Code) or
- (val instanceof Value and
- val.base?.unwrapAll() instanceof Code and
- val.properties.length is 1 and
- val.properties[0].name?.value in ['call', 'apply'])
- fn = val.base?.unwrapAll() or val
- ref = new Literal o.scope.freeVariable 'fn'
- base = new Value ref
- if val.base
- [val.base, base] = [base, val]
- body.expressions[idx] = new Call base, expr.args
- defs = defs.concat @makeCode(@tab), (new Assign(ref, fn).compileToFragments(o, LEVEL_TOP)), @makeCode(';\n')
- defs |
Switch | |
A JavaScript switch statement. Converts into a returnable expression on-demand. | exports.Switch = class Switch extends Base
- constructor: (@subject, @cases, @otherwise) ->
-
- children: ['subject', 'cases', 'otherwise']
-
- isStatement: YES
-
- jumps: (o = {block: yes}) ->
- for [conds, block] in @cases
- return block if block.jumps o
- @otherwise?.jumps o
-
- makeReturn: (res) ->
- pair[1].makeReturn res for pair in @cases
- @otherwise or= new Block [new Literal 'void 0'] if res
- @otherwise?.makeReturn res
- this
-
- compileNode: (o) ->
- idt1 = o.indent + TAB
- idt2 = o.indent = idt1 + TAB
- fragments = [].concat @makeCode(@tab + "switch ("),
- (if @subject then @subject.compileToFragments(o, LEVEL_PAREN) else @makeCode("false")),
- @makeCode(") {\n")
- for [conditions, block], i in @cases
- for cond in flatten [conditions]
- cond = cond.invert() unless @subject
- fragments = fragments.concat @makeCode(idt1 + "case "), cond.compileToFragments(o, LEVEL_PAREN), @makeCode(":\n")
- fragments = fragments.concat body, @makeCode('\n') if (body = block.compileToFragments o, LEVEL_TOP).length > 0
- break if i is @cases.length - 1 and not @otherwise
- expr = @lastNonComment block.expressions
- continue if expr instanceof Return or (expr instanceof Literal and expr.jumps() and expr.value isnt 'debugger')
- fragments.push cond.makeCode(idt2 + 'break;\n')
- if @otherwise and @otherwise.expressions.length
- fragments.push @makeCode(idt1 + "default:\n"), (@otherwise.compileToFragments o, LEVEL_TOP)..., @makeCode("\n")
- fragments.push @makeCode @tab + '}'
- fragments |
If | |
If/else statements. Acts as an expression by pushing down requested returns -to the last line of each clause. - +some cannot. + + + + + compileNode: (o) ->
+ body = Block.wrap [@body]
+ lastJumps = last(body.expressions)?.jumps()
+ @returns = no if lastJumps and lastJumps instanceof Return
+ source = if @range then @source.base else @source
+ scope = o.scope
+ name = @name and (@name.compile o, LEVEL_LIST)
+ index = @index and (@index.compile o, LEVEL_LIST)
+ scope.find(name) if name and not @pattern
+ scope.find(index) if index
+ rvar = scope.freeVariable 'results' if @returns
+ ivar = (@object and index) or scope.freeVariable 'i'
+ kvar = (@range and name) or index or ivar
+ kvarAssign = if kvar isnt ivar then "#{kvar} = " else ""
+ if @step and not @range
+ [step, stepVar] = @cacheToCodeFragments @step.cache o, LEVEL_LIST
+ stepNum = stepVar.match SIMPLENUM
+ name = ivar if @pattern
+ varPart = ''
+ guardPart = ''
+ defPart = ''
+ idt1 = @tab + TAB
+ if @range
+ forPartFragments = source.compileToFragments merge(o, {index: ivar, name, @step})
+ else
+ svar = @source.compile o, LEVEL_LIST
+ if (name or @own) and not IDENTIFIER.test svar
+ defPart += "#{@tab}#{ref = scope.freeVariable 'ref'} = #{svar};\n"
+ svar = ref
+ if name and not @pattern
+ namePart = "#{name} = #{svar}[#{kvar}]"
+ if not @object
+ defPart += "#{@tab}#{step};\n" if step isnt stepVar
+ lvar = scope.freeVariable 'len' unless @step and stepNum and down = (+stepNum < 0)
+ declare = "#{kvarAssign}#{ivar} = 0, #{lvar} = #{svar}.length"
+ declareDown = "#{kvarAssign}#{ivar} = #{svar}.length - 1"
+ compare = "#{ivar} < #{lvar}"
+ compareDown = "#{ivar} >= 0"
+ if @step
+ if stepNum
+ if down
+ compare = compareDown
+ declare = declareDown
+ else
+ compare = "#{stepVar} > 0 ? #{compare} : #{compareDown}"
+ declare = "(#{stepVar} > 0 ? (#{declare}) : #{declareDown})"
+ increment = "#{ivar} += #{stepVar}"
+ else
+ increment = "#{if kvar isnt ivar then "++#{ivar}" else "#{ivar}++"}"
+ forPartFragments = [@makeCode("#{declare}; #{compare}; #{kvarAssign}#{increment}")]
+ if @returns
+ resultPart = "#{@tab}#{rvar} = [];\n"
+ returnResult = "\n#{@tab}return #{rvar};"
+ body.makeReturn rvar
+ if @guard
+ if body.expressions.length > 1
+ body.expressions.unshift new If (new Parens @guard).invert(), new Literal "continue"
+ else
+ body = Block.wrap [new If @guard, body] if @guard
+ if @pattern
+ body.expressions.unshift new Assign @name, new Literal "#{svar}[#{kvar}]"
+ defPartFragments = [].concat @makeCode(defPart), @pluckDirectCall(o, body)
+ varPart = "\n#{idt1}#{namePart};" if namePart
+ if @object
+ forPartFragments = [@makeCode("#{kvar} in #{svar}")]
+ guardPart = "\n#{idt1}if (!#{utility 'hasProp'}.call(#{svar}, #{kvar})) continue;" if @own
+ bodyFragments = body.compileToFragments merge(o, indent: idt1), LEVEL_TOP
+ if bodyFragments and (bodyFragments.length > 0)
+ bodyFragments = [].concat @makeCode("\n"), bodyFragments, @makeCode("\n")
+ [].concat defPartFragments, @makeCode("#{resultPart or ''}#{@tab}for ("),
+ forPartFragments, @makeCode(") {#{guardPart}#{varPart}"), bodyFragments,
+ @makeCode("#{@tab}}#{returnResult or ''}")
+
+ pluckDirectCall: (o, body) ->
+ defs = []
+ for expr, idx in body.expressions
+ expr = expr.unwrapAll()
+ continue unless expr instanceof Call
+ val = expr.variable.unwrapAll()
+ continue unless (val instanceof Code) or
+ (val instanceof Value and
+ val.base?.unwrapAll() instanceof Code and
+ val.properties.length is 1 and
+ val.properties[0].name?.value in ['call', 'apply'])
+ fn = val.base?.unwrapAll() or val
+ ref = new Literal o.scope.freeVariable 'fn'
+ base = new Value ref
+ if val.base
+ [val.base, base] = [base, val]
+ body.expressions[idx] = new Call base, expr.args
+ defs = defs.concat @makeCode(@tab), (new Assign(ref, fn).compileToFragments(o, LEVEL_TOP)), @makeCode(';\n')
+ defs
+
+
+
+
+ ¶
+
+ Switch+ +
+
+
+
+
+ ¶
+
+ A JavaScript switch statement. Converts into a returnable expression on-demand. + + +exports.Switch = class Switch extends Base
+ constructor: (@subject, @cases, @otherwise) ->
+
+ children: ['subject', 'cases', 'otherwise']
+
+ isStatement: YES
+
+ jumps: (o = {block: yes}) ->
+ for [conds, block] in @cases
+ return block if block.jumps o
+ @otherwise?.jumps o
+
+ makeReturn: (res) ->
+ pair[1].makeReturn res for pair in @cases
+ @otherwise or= new Block [new Literal 'void 0'] if res
+ @otherwise?.makeReturn res
+ this
+
+ compileNode: (o) ->
+ idt1 = o.indent + TAB
+ idt2 = o.indent = idt1 + TAB
+ fragments = [].concat @makeCode(@tab + "switch ("),
+ (if @subject then @subject.compileToFragments(o, LEVEL_PAREN) else @makeCode "false"),
+ @makeCode(") {\n")
+ for [conditions, block], i in @cases
+ for cond in flatten [conditions]
+ cond = cond.invert() unless @subject
+ fragments = fragments.concat @makeCode(idt1 + "case "), cond.compileToFragments(o, LEVEL_PAREN), @makeCode(":\n")
+ fragments = fragments.concat body, @makeCode('\n') if (body = block.compileToFragments o, LEVEL_TOP).length > 0
+ break if i is @cases.length - 1 and not @otherwise
+ expr = @lastNonComment block.expressions
+ continue if expr instanceof Return or (expr instanceof Literal and expr.jumps() and expr.value isnt 'debugger')
+ fragments.push cond.makeCode(idt2 + 'break;\n')
+ if @otherwise and @otherwise.expressions.length
+ fragments.push @makeCode(idt1 + "default:\n"), (@otherwise.compileToFragments o, LEVEL_TOP)..., @makeCode("\n")
+ fragments.push @makeCode @tab + '}'
+ fragments
+
+
+
+
+ ¶
+
+ If+ +
+
+
+ ¶
+
+ If/else statements. Acts as an expression by pushing down requested returns +to the last line of each clause. + + Single-expression Ifs are compiled into conditional operators if possible, -because ternaries are already proper expressions, and don't need conversion. | exports.If = class If extends Base
- constructor: (condition, @body, options = {}) ->
- @condition = if options.type is 'unless' then condition.invert() else condition
- @elseBody = null
- @isChain = false
- {@soak} = options
-
- children: ['condition', 'body', 'elseBody']
-
- bodyNode: -> @body?.unwrap()
- elseBodyNode: -> @elseBody?.unwrap() |
Rewrite a chain of Ifs to add a default case as the final else. | addElse: (elseBody) ->
- if @isChain
- @elseBodyNode().addElse elseBody
- else
- @isChain = elseBody instanceof If
- @elseBody = @ensureBlock elseBody
- this |
The If only compiles into a statement if either of its bodies needs -to be a statement. Otherwise a conditional operator is safe. | isStatement: (o) ->
- o?.level is LEVEL_TOP or
- @bodyNode().isStatement(o) or @elseBodyNode()?.isStatement(o)
-
- jumps: (o) -> @body.jumps(o) or @elseBody?.jumps(o)
-
- compileNode: (o) ->
- if @isStatement o then @compileStatement o else @compileExpression o
-
- makeReturn: (res) ->
- @elseBody or= new Block [new Literal 'void 0'] if res
- @body and= new Block [@body.makeReturn res]
- @elseBody and= new Block [@elseBody.makeReturn res]
- this
-
- ensureBlock: (node) ->
- if node instanceof Block then node else new Block [node] |
Compile the | compileStatement: (o) ->
- child = del o, 'chainChild'
- exeq = del o, 'isExistentialEquals'
-
- if exeq
- return new If(@condition.invert(), @elseBodyNode(), type: 'if').compileToFragments o
-
- indent = o.indent + TAB
- cond = @condition.compileToFragments o, LEVEL_PAREN
- body = @ensureBlock(@body).compileToFragments merge o, {indent}
- ifPart = [].concat @makeCode("if ("), cond, @makeCode(") {\n"), body, @makeCode("\n#{@tab}}")
- ifPart.unshift @makeCode @tab unless child
- return ifPart unless @elseBody
- answer = ifPart.concat @makeCode(' else ')
- if @isChain
- o.chainChild = yes
- answer = answer.concat @elseBody.unwrap().compileToFragments o, LEVEL_TOP
- else
- answer = answer.concat @makeCode("{\n"), @elseBody.compileToFragments(merge(o, {indent}), LEVEL_TOP), @makeCode("\n#{@tab}}")
- answer |
Compile the | compileExpression: (o) ->
- cond = @condition.compileToFragments o, LEVEL_COND
- body = @bodyNode().compileToFragments o, LEVEL_LIST
- alt = if @elseBodyNode() then @elseBodyNode().compileToFragments(o, LEVEL_LIST) else [@makeCode('void 0')]
- fragments = cond.concat @makeCode(" ? "), body, @makeCode(" : "), alt
- if o.level >= LEVEL_COND then @wrapInBraces fragments else fragments
-
- unfoldSoak: ->
- @soak and this |
Faux-Nodes- -Faux-nodes are never created by the grammar, but are used during code -generation to generate other combinations of nodes. | |
Closure | |
A faux-node used to wrap an expressions body in a closure. | Closure = |
Wrap the expressions body, unless it contains a pure statement, +because ternaries are already proper expressions, and don't need conversion. + + + + +exports.If = class If extends Base
+ constructor: (condition, @body, options = {}) ->
+ @condition = if options.type is 'unless' then condition.invert() else condition
+ @elseBody = null
+ @isChain = false
+ {@soak} = options
+
+ children: ['condition', 'body', 'elseBody']
+
+ bodyNode: -> @body?.unwrap()
+ elseBodyNode: -> @elseBody?.unwrap()
+
+
+
+
+ ¶
+
+ Rewrite a chain of Ifs to add a default case as the final else. + + + addElse: (elseBody) ->
+ if @isChain
+ @elseBodyNode().addElse elseBody
+ else
+ @isChain = elseBody instanceof If
+ @elseBody = @ensureBlock elseBody
+ this
+
+
+
+
+ ¶
+
+ The If only compiles into a statement if either of its bodies needs +to be a statement. Otherwise a conditional operator is safe. + + + isStatement: (o) ->
+ o?.level is LEVEL_TOP or
+ @bodyNode().isStatement(o) or @elseBodyNode()?.isStatement(o)
+
+ jumps: (o) -> @body.jumps(o) or @elseBody?.jumps(o)
+
+ compileNode: (o) ->
+ if @isStatement o then @compileStatement o else @compileExpression o
+
+ makeReturn: (res) ->
+ @elseBody or= new Block [new Literal 'void 0'] if res
+ @body and= new Block [@body.makeReturn res]
+ @elseBody and= new Block [@elseBody.makeReturn res]
+ this
+
+ ensureBlock: (node) ->
+ if node instanceof Block then node else new Block [node]
+
+
+
+
+ ¶
+
+ Compile the compileStatement: (o) ->
+ child = del o, 'chainChild'
+ exeq = del o, 'isExistentialEquals'
+
+ if exeq
+ return new If(@condition.invert(), @elseBodyNode(), type: 'if').compileToFragments o
+
+ indent = o.indent + TAB
+ cond = @condition.compileToFragments o, LEVEL_PAREN
+ body = @ensureBlock(@body).compileToFragments merge o, {indent}
+ ifPart = [].concat @makeCode("if ("), cond, @makeCode(") {\n"), body, @makeCode("\n#{@tab}}")
+ ifPart.unshift @makeCode @tab unless child
+ return ifPart unless @elseBody
+ answer = ifPart.concat @makeCode(' else ')
+ if @isChain
+ o.chainChild = yes
+ answer = answer.concat @elseBody.unwrap().compileToFragments o, LEVEL_TOP
+ else
+ answer = answer.concat @makeCode("{\n"), @elseBody.compileToFragments(merge(o, {indent}), LEVEL_TOP), @makeCode("\n#{@tab}}")
+ answer
+
+
+
+
+ ¶
+
+ Compile the compileExpression: (o) ->
+ cond = @condition.compileToFragments o, LEVEL_COND
+ body = @bodyNode().compileToFragments o, LEVEL_LIST
+ alt = if @elseBodyNode() then @elseBodyNode().compileToFragments(o, LEVEL_LIST) else [@makeCode('void 0')]
+ fragments = cond.concat @makeCode(" ? "), body, @makeCode(" : "), alt
+ if o.level >= LEVEL_COND then @wrapInBraces fragments else fragments
+
+ unfoldSoak: ->
+ @soak and this
+
+
+
+
+ ¶
+
+ Faux-Nodes+ +
+
+
+
+
+ ¶
+
+ Faux-nodes are never created by the grammar, but are used during code +generation to generate other combinations of nodes. + + +
+
+
+
+
+ ¶
+
+ Closure+ +
+
+
+
+
+ ¶
+
+ A faux-node used to wrap an expressions body in a closure. + + +Closure =
+
+
+ ¶
+
+ Wrap the expressions body, unless it contains a pure statement,
in which case, no dice. If the body mentions | wrap: (expressions, statement, noReturn) ->
- return expressions if expressions.jumps()
- func = new Code [], Block.wrap [expressions]
- args = []
- if (mentionsArgs = expressions.contains @literalArgs) or expressions.contains @literalThis
- if mentionsArgs and expressions.classBody
- throw SyntaxError "Class bodies shouldn't reference arguments"
- meth = new Literal if mentionsArgs then 'apply' else 'call'
- args = [new Literal 'this']
- args.push new Literal 'arguments' if mentionsArgs
- func = new Value func, [new Access meth]
- func.noReturn = noReturn
- call = new Call func, args
- if statement then Block.wrap [call] else call
-
- literalArgs: (node) ->
- node instanceof Literal and node.value is 'arguments' and not node.asKey
-
- literalThis: (node) ->
- (node instanceof Literal and node.value is 'this' and not node.asKey) or
- (node instanceof Code and node.bound) or
- (node instanceof Call and node.isSuper) |
Unfold a node's child if soak, then tuck the node under created | unfoldSoak = (o, parent, name) ->
- return unless ifn = parent[name].unfoldSoak o
- parent[name] = ifn.body
- ifn.body = new Value parent
- ifn |
Constants | UTILITIES = |
Correctly set up a prototype chain for inheritance, including a reference
-to the superclass for | extends: -> """
- function(child, parent) { for (var key in parent) { if (#{utility 'hasProp'}.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }
- """ |
Discover if an item is in an array. | indexOf: -> """
- [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }
- """ |
Shortcuts to speed up the lookup time for native functions. | hasProp: -> '{}.hasOwnProperty'
- slice : -> '[].slice' |
Levels indicate a node's position in the AST. Useful for knowing if -parens are necessary or superfluous. | LEVEL_TOP = 1 # ...;
-LEVEL_PAREN = 2 # (...)
-LEVEL_LIST = 3 # [...]
-LEVEL_COND = 4 # ... ? x : y
-LEVEL_OP = 5 # !...
-LEVEL_ACCESS = 6 # ...[0] |
Tabs are two spaces for pretty printing. | TAB = ' '
-
-IDENTIFIER_STR = "[$A-Za-z_\\x7f-\\uffff][$\\w\\x7f-\\uffff]*"
-IDENTIFIER = /// ^ #{IDENTIFIER_STR} $ ///
-SIMPLENUM = /^[+-]?\d+$/
-METHOD_DEF = ///
- ^
- (?:
- (#{IDENTIFIER_STR})
- \.prototype
- (?:
- \.(#{IDENTIFIER_STR})
- | \[("(?:[^\\"\r\n]|\\.)*"|'(?:[^\\'\r\n]|\\.)*')\]
- | \[(0x[\da-fA-F]+ | \d*\.?\d+ (?:[eE][+-]?\d+)?)\]
- )
- )
- |
- (#{IDENTIFIER_STR})
- $
-/// |
Is a literal value a string? | IS_STRING = /^['"]/ |
Utility Functions | |
Helper for ensuring that utility functions are assigned at the top level. | utility = (name) ->
- ref = "__#{name}"
- Scope.root.assign ref, UTILITIES[name]()
- ref
-
-multident = (code, tab) ->
- code = code.replace /\n/g, '$&' + tab
- code.replace /\s+$/, ''
-
- |
wrap: (expressions, statement, noReturn) ->
+ return expressions if expressions.jumps()
+ func = new Code [], Block.wrap [expressions]
+ args = []
+ argumentsNode = expressions.contains @isLiteralArguments
+ if argumentsNode and expressions.classBody
+ argumentsNode.error "Class bodies shouldn't reference arguments"
+ if argumentsNode or expressions.contains @isLiteralThis
+ meth = new Literal if argumentsNode then 'apply' else 'call'
+ args = [new Literal 'this']
+ args.push new Literal 'arguments' if argumentsNode
+ func = new Value func, [new Access meth]
+ func.noReturn = noReturn
+ call = new Call func, args
+ if statement then Block.wrap [call] else call
+
+ isLiteralArguments: (node) ->
+ node instanceof Literal and node.value is 'arguments' and not node.asKey
+
+ isLiteralThis: (node) ->
+ (node instanceof Literal and node.value is 'this' and not node.asKey) or
+ (node instanceof Code and node.bound) or
+ (node instanceof Call and node.isSuper)
Unfold a node's child if soak, then tuck the node under created If
+
unfoldSoak = (o, parent, name) ->
+ return unless ifn = parent[name].unfoldSoak o
+ parent[name] = ifn.body
+ ifn.body = new Value parent
+ ifn
+UTILITIES =
Correctly set up a prototype chain for inheritance, including a reference
+to the superclass for super()
calls, and copies of any static properties.
+
extends: -> """
+ function(child, parent) { for (var key in parent) { if (#{utility 'hasProp'}.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }
+ """
Create a function bound to the current value of "this". +
+ + bind: -> '''
+ function(fn, me){ return function(){ return fn.apply(me, arguments); }; }
+ '''
Discover if an item is in an array. +
+ + indexOf: -> """
+ [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }
+ """
Shortcuts to speed up the lookup time for native functions. +
+ + hasProp: -> '{}.hasOwnProperty'
+ slice : -> '[].slice'
Levels indicate a node's position in the AST. Useful for knowing if +parens are necessary or superfluous. +
+ +LEVEL_TOP = 1 # ...;
+LEVEL_PAREN = 2 # (...)
+LEVEL_LIST = 3 # [...]
+LEVEL_COND = 4 # ... ? x : y
+LEVEL_OP = 5 # !...
+LEVEL_ACCESS = 6 # ...[0]
Tabs are two spaces for pretty printing. +
+ +TAB = ' '
+
+IDENTIFIER_STR = "[$A-Za-z_\\x7f-\\uffff][$\\w\\x7f-\\uffff]*"
+IDENTIFIER = /// ^ #{IDENTIFIER_STR} $ ///
+SIMPLENUM = /^[+-]?\d+$/
+METHOD_DEF = ///
+ ^
+ (?:
+ (#{IDENTIFIER_STR})
+ \.prototype
+ (?:
+ \.(#{IDENTIFIER_STR})
+ | \[("(?:[^\\"\r\n]|\\.)*"|'(?:[^\\'\r\n]|\\.)*')\]
+ | \[(0x[\da-fA-F]+ | \d*\.?\d+ (?:[eE][+-]?\d+)?)\]
+ )
+ )
+ |
+ (#{IDENTIFIER_STR})
+ $
+///
Is a literal value a string? +
+ +IS_STRING = /^['"]/
Helper for ensuring that utility functions are assigned at the top level. +
+ +utility = (name) ->
+ ref = "__#{name}"
+ Scope.root.assign ref, UTILITIES[name]()
+ ref
+
+multident = (code, tab) ->
+ code = code.replace /\n/g, '$&' + tab
+ code.replace /\s+$/, ''
optparse.coffee | |
---|---|
A simple OptionParser class to parse option flags from the command-line. -Use it like so: + -
+
+
+
+
+
+
| exports.OptionParser = class OptionParser |
Initialize with a list of valid options, in the form: +option) list, and all subsequent arguments are left unparsed. + + + + +exports.OptionParser = class OptionParser
+
+
+ ¶
+
+ Initialize with a list of valid options, in the form: -
+
+
+Along with an an optional banner for the usage help. + -Along with an an optional banner for the usage help. | constructor: (rules, @banner) ->
- @rules = buildRules rules |
Parse the list of arguments, populating an constructor: (rules, @banner) ->
+ @rules = buildRules rules
+
+
+ ¶
+
+ Parse the list of arguments, populating an | parse: (args) ->
- options = arguments: []
- skippingArgument = no
- originalArgs = args
- args = normalizeArguments args
- for arg, i in args
- if skippingArgument
- skippingArgument = no
- continue
- if arg is '--'
- pos = originalArgs.indexOf '--'
- options.arguments = options.arguments.concat originalArgs[(pos + 1)..]
- break
- isOption = !!(arg.match(LONG_FLAG) or arg.match(SHORT_FLAG)) |
the CS option parser is a little odd; options after the first -non-option argument are treated as non-option arguments themselves | seenNonOptionArg = options.arguments.length > 0
- unless seenNonOptionArg
- matchedRule = no
- for rule in @rules
- if rule.shortFlag is arg or rule.longFlag is arg
- value = true
- if rule.hasArgument
- skippingArgument = yes
- value = args[i + 1]
- options[rule.name] = if rule.isList then (options[rule.name] or []).concat value else value
- matchedRule = yes
- break
- throw new Error "unrecognized option: #{arg}" if isOption and not matchedRule
- if seenNonOptionArg or not isOption
- options.arguments.push arg
- options |
Return the help text for this OptionParser, listing and describing all
-of the valid options, for | help: ->
- lines = []
- lines.unshift "#{@banner}\n" if @banner
- for rule in @rules
- spaces = 15 - rule.longFlag.length
- spaces = if spaces > 0 then Array(spaces + 1).join(' ') else ''
- letPart = if rule.shortFlag then rule.shortFlag + ', ' else ' '
- lines.push ' ' + letPart + rule.longFlag + spaces + rule.description
- "\n#{ lines.join('\n') }\n" |
Helpers | |
Regex matchers for option flags. | LONG_FLAG = /^(--\w[\w\-]*)/
-SHORT_FLAG = /^(-\w)$/
-MULTI_FLAG = /^-(\w{2,})/
-OPTIONAL = /\[(\w+(\*?))\]/ |
Build and return the list of option rules. If the optional short-flag is
-unspecified, leave it out by padding with | buildRules = (rules) ->
- for tuple in rules
- tuple.unshift null if tuple.length < 3
- buildRule tuple... |
Build a rule from a | buildRule = (shortFlag, longFlag, description, options = {}) ->
- match = longFlag.match(OPTIONAL)
- longFlag = longFlag.match(LONG_FLAG)[1]
- {
- name: longFlag.substr 2
- shortFlag: shortFlag
- longFlag: longFlag
- description: description
- hasArgument: !!(match and match[1])
- isList: !!(match and match[2])
- } |
Normalize arguments by expanding merged flags into multiple flags. This allows
-you to have | normalizeArguments = (args) ->
- args = args[..]
- result = []
- for arg in args
- if match = arg.match MULTI_FLAG
- result.push '-' + l for l in match[1].split ''
- else
- result.push arg
- result
+you're responsible for interpreting the options object.
+
+
+ parse: (args) ->
+ options = arguments: []
+ skippingArgument = no
+ originalArgs = args
+ args = normalizeArguments args
+ for arg, i in args
+ if skippingArgument
+ skippingArgument = no
+ continue
+ if arg is '--'
+ pos = originalArgs.indexOf '--'
+ options.arguments = options.arguments.concat originalArgs[(pos + 1)..]
+ break
+ isOption = !!(arg.match(LONG_FLAG) or arg.match(SHORT_FLAG))
+
+
+
+
+ ¶
+
+ the CS option parser is a little odd; options after the first +non-option argument are treated as non-option arguments themselves + + + seenNonOptionArg = options.arguments.length > 0
+ unless seenNonOptionArg
+ matchedRule = no
+ for rule in @rules
+ if rule.shortFlag is arg or rule.longFlag is arg
+ value = true
+ if rule.hasArgument
+ skippingArgument = yes
+ value = args[i + 1]
+ options[rule.name] = if rule.isList then (options[rule.name] or []).concat value else value
+ matchedRule = yes
+ break
+ throw new Error "unrecognized option: #{arg}" if isOption and not matchedRule
+ if seenNonOptionArg or not isOption
+ options.arguments.push arg
+ options
+
+
+
+
+ ¶
+
+ Return the help text for this OptionParser, listing and describing all
+of the valid options, for help: ->
+ lines = []
+ lines.unshift "#{@banner}\n" if @banner
+ for rule in @rules
+ spaces = 15 - rule.longFlag.length
+ spaces = if spaces > 0 then repeat ' ', spaces else ''
+ letPart = if rule.shortFlag then rule.shortFlag + ', ' else ' '
+ lines.push ' ' + letPart + rule.longFlag + spaces + rule.description
+ "\n#{ lines.join('\n') }\n"
+
+
+
+
+ ¶
+
+ Helpers+ +
+
+
+
+
+ ¶
+
+
+
+
+
+
+
+ ¶
+
+ Regex matchers for option flags. + + +LONG_FLAG = /^(--\w[\w\-]*)/
+SHORT_FLAG = /^(-\w)$/
+MULTI_FLAG = /^-(\w{2,})/
+OPTIONAL = /\[(\w+(\*?))\]/
+
+
+
+
+ ¶
+
+ Build and return the list of option rules. If the optional short-flag is
+unspecified, leave it out by padding with buildRules = (rules) ->
+ for tuple in rules
+ tuple.unshift null if tuple.length < 3
+ buildRule tuple...
+
+
+
+
+ ¶
+
+ Build a rule from a buildRule = (shortFlag, longFlag, description, options = {}) ->
+ match = longFlag.match(OPTIONAL)
+ longFlag = longFlag.match(LONG_FLAG)[1]
+ {
+ name: longFlag.substr 2
+ shortFlag: shortFlag
+ longFlag: longFlag
+ description: description
+ hasArgument: !!(match and match[1])
+ isList: !!(match and match[2])
+ }
+
+
+ ¶
+
+ Normalize arguments by expanding merged flags into multiple flags. This allows
+you to have |
normalizeArguments = (args) ->
+ args = args[..]
+ result = []
+ for arg in args
+ if match = arg.match MULTI_FLAG
+ result.push '-' + l for l in match[1].split ''
+ else
+ result.push arg
+ result
repl.coffee | |
---|---|
vm = require 'vm'
-nodeREPL = require 'repl'
-CoffeeScript = require './coffee-script'
-{merge} = require './helpers'
-
-replDefaults =
- prompt: 'coffee> ',
- eval: (input, context, filename, cb) -> | |
XXX: multiline hack | input = input.replace /\uFF00/g, '\n' |
strip single-line comments | input = input.replace /(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, '$1$2$3' |
empty command | return cb null if /^(\s*|\(\s*\))$/.test input |
TODO: fix #1829: pass in-scope vars and avoid accidentally shadowing them by omitting those declarations | try
- js = CoffeeScript.compile "_=(#{input}\n)", {filename, bare: yes}
- cb null, vm.runInContext(js, context, filename)
- catch err
- cb err
-
-addMultilineHandler = (repl) ->
- {rli, inputStream, outputStream} = repl
-
- multiline =
- enabled: off
- initialPrompt: repl.prompt.replace(/^[^> ]*/, (x) -> x.replace /./g, '-')
- prompt: repl.prompt.replace(/^[^> ]*>?/, (x) -> x.replace /./g, '.')
- buffer: '' |
Proxy node's line listener | nodeLineListener = rli.listeners('line')[0]
- rli.removeListener 'line', nodeLineListener
- rli.on 'line', (cmd) ->
- if multiline.enabled
- multiline.buffer += "#{cmd}\n"
- rli.setPrompt multiline.prompt
- rli.prompt true
- else
- nodeLineListener cmd
- return |
Handle Ctrl-v | inputStream.on 'keypress', (char, key) ->
- return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
- if multiline.enabled |
allow arbitrarily switching between modes any time before multiple lines are entered | unless multiline.buffer.match /\n/
- multiline.enabled = not multiline.enabled
- rli.setPrompt repl.prompt
- rli.prompt true
- return |
no-op unless the current line is empty | return if rli.line? and not rli.line.match /^\s*$/ |
eval, print, loop | multiline.enabled = not multiline.enabled
- rli.line = ''
- rli.cursor = 0
- rli.output.cursorTo 0
- rli.output.clearLine 1 |
XXX: multiline hack | multiline.buffer = multiline.buffer.replace /\n/g, '\uFF00'
- rli.emit 'line', multiline.buffer
- multiline.buffer = ''
- else
- multiline.enabled = not multiline.enabled
- rli.setPrompt multiline.initialPrompt
- rli.prompt true
- return
-
-module.exports =
- start: (opts = {}) ->
- opts = merge replDefaults, opts
- repl = nodeREPL.start opts
- repl.on 'exit', -> repl.outputStream.write '\n'
- addMultilineHandler repl
- repl
-
- |
fs = require 'fs'
+path = require 'path'
+vm = require 'vm'
+nodeREPL = require 'repl'
+CoffeeScript = require './coffee-script'
+{merge, prettyErrorMessage} = require './helpers'
+
+replDefaults =
+ prompt: 'coffee> ',
+ historyFile: path.join process.env.HOME, '.coffee_history' if process.env.HOME
+ historyMaxInputSize: 10240
+ eval: (input, context, filename, cb) ->
XXX: multiline hack. +
+ + input = input.replace /\uFF00/g, '\n'
Node's REPL sends the input ending with a newline and then wrapped in +parens. Unwrap all that. +
+ + input = input.replace /^\(([\s\S]*)\n\)$/m, '$1'
Require AST nodes to do some AST manipulation. +
+ + {Block, Assign, Value, Literal} = require './nodes'
+
+ try
Generate the AST of the clean input. +
+ + ast = CoffeeScript.nodes input
Add assignment to _
variable to force the input to be an expression.
+
ast = new Block [
+ new Assign (new Value new Literal '_'), ast, '='
+ ]
+ js = ast.compile bare: yes, locals: Object.keys(context)
+ cb null, vm.runInContext(js, context, filename)
+ catch err
+ cb prettyErrorMessage(err, filename, input, yes)
+
+addMultilineHandler = (repl) ->
+ {rli, inputStream, outputStream} = repl
+
+ multiline =
+ enabled: off
+ initialPrompt: repl.prompt.replace /^[^> ]*/, (x) -> x.replace /./g, '-'
+ prompt: repl.prompt.replace /^[^> ]*>?/, (x) -> x.replace /./g, '.'
+ buffer: ''
Proxy node's line listener +
+ + nodeLineListener = rli.listeners('line')[0]
+ rli.removeListener 'line', nodeLineListener
+ rli.on 'line', (cmd) ->
+ if multiline.enabled
+ multiline.buffer += "#{cmd}\n"
+ rli.setPrompt multiline.prompt
+ rli.prompt true
+ else
+ nodeLineListener cmd
+ return
Handle Ctrl-v +
+ + inputStream.on 'keypress', (char, key) ->
+ return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
+ if multiline.enabled
allow arbitrarily switching between modes any time before multiple lines are entered +
+ + unless multiline.buffer.match /\n/
+ multiline.enabled = not multiline.enabled
+ rli.setPrompt repl.prompt
+ rli.prompt true
+ return
no-op unless the current line is empty +
+ + return if rli.line? and not rli.line.match /^\s*$/
eval, print, loop +
+ + multiline.enabled = not multiline.enabled
+ rli.line = ''
+ rli.cursor = 0
+ rli.output.cursorTo 0
+ rli.output.clearLine 1
XXX: multiline hack +
+ + multiline.buffer = multiline.buffer.replace /\n/g, '\uFF00'
+ rli.emit 'line', multiline.buffer
+ multiline.buffer = ''
+ else
+ multiline.enabled = not multiline.enabled
+ rli.setPrompt multiline.initialPrompt
+ rli.prompt true
+ return
Store and load command history from a file +
+ +addHistory = (repl, filename, maxSize) ->
+ lastLine = null
+ try
Get file info and at most maxSize of command history +
+ + stat = fs.statSync filename
+ size = Math.min maxSize, stat.size
Read last size
bytes from the file
+
readFd = fs.openSync filename, 'r'
+ buffer = new Buffer(size)
+ fs.readSync readFd, buffer, 0, size, stat.size - size
Set the history on the interpreter +
+ + repl.rli.history = buffer.toString().split('\n').reverse()
If the history file was truncated we should pop off a potential partial line +
+ + repl.rli.history.pop() if stat.size > maxSize
Shift off the final blank newline +
+ + repl.rli.history.shift() if repl.rli.history[0] is ''
+ repl.rli.historyIndex = -1
+ lastLine = repl.rli.history[0]
+
+ fd = fs.openSync filename, 'a'
+
+ repl.rli.addListener 'line', (code) ->
+ if code and code.length and code isnt '.history' and lastLine isnt code
Save the latest command in the file +
+ + fs.write fd, "#{code}\n"
+ lastLine = code
+
+ repl.rli.on 'exit', -> fs.close fd
Add a command to show the history stack +
+ + repl.commands['.history'] =
+ help: 'Show command history'
+ action: ->
+ repl.outputStream.write "#{repl.rli.history[..].reverse().join '\n'}\n"
+ repl.displayPrompt()
+
+module.exports =
+ start: (opts = {}) ->
+ [major, minor, build] = process.versions.node.split('.').map (n) -> parseInt(n)
+
+ if major is 0 and minor < 8
+ console.warn "Node 0.8.0+ required for CoffeeScript REPL"
+ process.exit 1
+
+ opts = merge replDefaults, opts
+ repl = nodeREPL.start opts
+ repl.on 'exit', -> repl.outputStream.write '\n'
+ addMultilineHandler repl
+ addHistory repl, opts.historyFile, opts.historyMaxInputSize if opts.historyFile
+ repl
rewriter.coffee | |
---|---|
The CoffeeScript language has a good deal of optional syntax, implicit syntax, + + + + +
+
+
+
| |
Create a generated token: one that exists due to a use of implicit syntax. | generate = (tag, value) ->
- tok = [tag, value]
- tok.generated = yes
- tok |
The Rewriter class is used by the Lexer, directly against -its internal array of tokens. | class exports.Rewriter |
Helpful snippet for debugging: - console.log (t[0] + '/' + t[1] for t in @tokens).join ' ' | |
Rewrite the token stream in multiple passes, one logical filter at +parentheses, and generally clean things up. + + + + + + + +
+
+
+
+
+ ¶
+
+ Create a generated token: one that exists due to a use of implicit syntax. + + +generate = (tag, value) ->
+ tok = [tag, value]
+ tok.generated = yes
+ tok
+
+
+
+
+ ¶
+
+ The Rewriter class is used by the Lexer, directly against +its internal array of tokens. + + +class exports.Rewriter
+
+
+
+
+ ¶
+
+ Helpful snippet for debugging: + + +
+
+
+
+
+ ¶
+
+ 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 to work with +stream, with a big ol' efficient switch, but it's much nicer to work with like this. The order of these passes matters -- indentation must be -corrected before implicit parentheses can be wrapped around blocks of code. | rewrite: (@tokens) ->
- @removeLeadingNewlines()
- @removeMidExpressionNewlines()
- @closeOpenCalls()
- @closeOpenIndexes()
- @addImplicitIndentation()
- @tagPostfixConditionals()
- @addImplicitBracesAndParens()
- @addLocationDataToGeneratedTokens()
- @tokens |
Rewrite the token stream, looking one token ahead and behind. +corrected before implicit parentheses can be wrapped around blocks of code. + + + + + rewrite: (@tokens) ->
+ @removeLeadingNewlines()
+ @removeMidExpressionNewlines()
+ @closeOpenCalls()
+ @closeOpenIndexes()
+ @addImplicitIndentation()
+ @tagPostfixConditionals()
+ @addImplicitBracesAndParens()
+ @addLocationDataToGeneratedTokens()
+ @tokens
+
+
+ ¶
+
+ 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 +forwards (or backwards) in the stream, to make sure we don't miss anything as tokens are inserted and removed, and the stream changes length under -our feet. | scanTokens: (block) ->
- {tokens} = this
- i = 0
- i += block.call this, token, i, tokens while token = tokens[i]
- true
-
- detectEnd: (i, condition, action) ->
- {tokens} = this
- levels = 0
- while token = tokens[i]
- return action.call this, token, i if levels is 0 and condition.call this, token, i
- return action.call this, token, i - 1 if not token or levels < 0
- if token[0] in EXPRESSION_START
- levels += 1
- else if token[0] in EXPRESSION_END
- levels -= 1
- i += 1
- i - 1 |
Leading newlines would introduce an ambiguity in the grammar, so we -dispatch them here. | removeLeadingNewlines: ->
- break for [tag], i in @tokens when tag isnt 'TERMINATOR'
- @tokens.splice 0, i if i |
Some blocks occur in the middle of expressions -- when we're expecting -this, remove their trailing newlines. | removeMidExpressionNewlines: ->
- @scanTokens (token, i, tokens) ->
- return 1 unless token[0] is 'TERMINATOR' and @tag(i + 1) in EXPRESSION_CLOSE
- tokens.splice i, 1
- 0 |
The lexer has tagged the opening parenthesis of a method call. Match it with +our feet. + + + + + scanTokens: (block) ->
+ {tokens} = this
+ i = 0
+ i += block.call this, token, i, tokens while token = tokens[i]
+ true
+
+ detectEnd: (i, condition, action) ->
+ {tokens} = this
+ levels = 0
+ while token = tokens[i]
+ return action.call this, token, i if levels is 0 and condition.call this, token, i
+ return action.call this, token, i - 1 if not token or levels < 0
+ if token[0] in EXPRESSION_START
+ levels += 1
+ else if token[0] in EXPRESSION_END
+ levels -= 1
+ i += 1
+ i - 1
+
+
+
+
+ ¶
+
+ Leading newlines would introduce an ambiguity in the grammar, so we +dispatch them here. + + + removeLeadingNewlines: ->
+ break for [tag], i in @tokens when tag isnt 'TERMINATOR'
+ @tokens.splice 0, i if i
+
+
+
+
+ ¶
+
+ Some blocks occur in the middle of expressions -- when we're expecting +this, remove their trailing newlines. + + + removeMidExpressionNewlines: ->
+ @scanTokens (token, i, tokens) ->
+ return 1 unless token[0] is 'TERMINATOR' and @tag(i + 1) in EXPRESSION_CLOSE
+ tokens.splice i, 1
+ 0
+
+
+ ¶
+
+ The lexer has tagged the opening parenthesis of a method call. Match it with its paired close. We have the mis-nested outdent case included here for -calls that close on the same line, just before their outdent. | closeOpenCalls: ->
- condition = (token, i) ->
- token[0] in [')', 'CALL_END'] or
- token[0] is 'OUTDENT' and @tag(i - 1) is ')'
-
- action = (token, i) ->
- @tokens[if token[0] is 'OUTDENT' then i - 1 else i][0] = 'CALL_END'
-
- @scanTokens (token, i) ->
- @detectEnd i + 1, condition, action if token[0] is 'CALL_START'
- 1 |
The lexer has tagged the opening parenthesis of an indexing operation call. -Match it with its paired close. | closeOpenIndexes: ->
- condition = (token, i) ->
- token[0] in [']', 'INDEX_END']
-
- action = (token, i) ->
- token[0] = 'INDEX_END'
-
- @scanTokens (token, i) ->
- @detectEnd i + 1, condition, action if token[0] is 'INDEX_START'
- 1 |
Match tags in token stream starting at i with pattern, skipping HERECOMMENTs +calls that close on the same line, just before their outdent. + + + + + closeOpenCalls: ->
+ condition = (token, i) ->
+ token[0] in [')', 'CALL_END'] or
+ token[0] is 'OUTDENT' and @tag(i - 1) is ')'
+
+ action = (token, i) ->
+ @tokens[if token[0] is 'OUTDENT' then i - 1 else i][0] = 'CALL_END'
+
+ @scanTokens (token, i) ->
+ @detectEnd i + 1, condition, action if token[0] is 'CALL_START'
+ 1
+
+
+
+
+ ¶
+
+ The lexer has tagged the opening parenthesis of an indexing operation call. +Match it with its paired close. + + + closeOpenIndexes: ->
+ condition = (token, i) ->
+ token[0] in [']', 'INDEX_END']
+
+ action = (token, i) ->
+ token[0] = 'INDEX_END'
+
+ @scanTokens (token, i) ->
+ @detectEnd i + 1, condition, action if token[0] is 'INDEX_START'
+ 1
+
+
+ ¶
+
+ Match tags in token stream starting at i with pattern, skipping HERECOMMENTs Pattern may consist of strings (equality), an array of strings (one of) -or null (wildcard) | matchTags: (i, pattern...) ->
- fuzz = 0
- for j in [0 ... pattern.length]
- fuzz += 2 while @tag(i + j + fuzz) is 'HERECOMMENT'
- continue if not pattern[j]?
- pattern[j] = [pattern[j]] if typeof pattern[j] is 'string'
- return no if @tag(i + j + fuzz) not in pattern[j]
- yes |
yes iff standing in front of something looking like
-@ | looksObjectish: (j) ->
- @matchTags(j, '@', null, ':') or @matchTags(j, null, ':') |
yes iff current line of tokens contain an element of tags on same +or null (wildcard) + + + + + matchTags: (i, pattern...) ->
+ fuzz = 0
+ for j in [0 ... pattern.length]
+ fuzz += 2 while @tag(i + j + fuzz) is 'HERECOMMENT'
+ continue if not pattern[j]?
+ pattern[j] = [pattern[j]] if typeof pattern[j] is 'string'
+ return no if @tag(i + j + fuzz) not in pattern[j]
+ yes
+
+
+
+
+ ¶
+
+ yes iff standing in front of something looking like
+@ looksObjectish: (j) ->
+ @matchTags(j, '@', null, ':') or @matchTags(j, null, ':')
+
+
+ ¶
+
+ yes iff current line of tokens contain an element of tags on same expression level. Stop searching at LINEBREAKS or explicit start of -containing balanced expression. | findTagsBackwards: (i, tags) ->
- backStack = []
- while i >= 0 and (backStack.length or
- @tag(i) not in tags and
- (@tag(i) not in EXPRESSION_START or @tokens[i].generated) and
- @tag(i) not in LINEBREAKS)
- backStack.push @tag(i) if @tag(i) in EXPRESSION_END
- backStack.pop() if @tag(i) in EXPRESSION_START and backStack.length
- i -= 1
- @tag(i) in tags |
Look for signs of implicit calls and objects in the token stream and -add them. | addImplicitBracesAndParens: -> |
Track current balancing depth (both implicit and explicit) on stack. | stack = []
-
- @scanTokens (token, i, tokens) ->
- [tag] = token
- [prevTag] = if i > 0 then tokens[i - 1] else []
- [nextTag] = if i < tokens.length - 1 then tokens[i + 1] else []
- stackTop = -> stack[stack.length - 1]
- startIdx = i |
Helper function, used for keeping track of the number of tokens consumed -and spliced, when returning for getting a new token. | forward = (n) -> i - startIdx + n |
Helper functions | inImplicit = -> stackTop()?[2]?.ours
- inImplicitCall = -> inImplicit() and stackTop()?[0] is '('
- inImplicitObject = -> inImplicit() and stackTop()?[0] is '{' |
Unclosed control statement inside implicit parens (like -class declaration or if-conditionals) | inImplicitControl = -> inImplicit and stackTop()?[0] is 'CONTROL'
-
- startImplicitCall = (j) ->
- idx = j ? i
- stack.push ['(', idx, ours: yes]
- tokens.splice idx, 0, generate 'CALL_START', '('
- i += 1 if not j?
-
- endImplicitCall = ->
- stack.pop()
- tokens.splice i, 0, generate 'CALL_END', ')'
- i += 1
-
- startImplicitObject = (j, startsLine = yes) ->
- idx = j ? i
- stack.push ['{', idx, sameLine: yes, startsLine: startsLine, ours: yes]
- tokens.splice idx, 0, generate '{', generate(new String('{'))
- i += 1 if not j?
-
- endImplicitObject = (j) ->
- j = j ? i
- stack.pop()
- tokens.splice j, 0, generate '}', '}'
- i += 1 |
Don't end an implicit call on next indent if any of these are in an argument | if inImplicitCall() and tag in ['IF', 'TRY', 'FINALLY', 'CATCH',
- 'CLASS', 'SWITCH']
- stack.push ['CONTROL', i, ours: true]
- return forward(1)
-
- if tag is 'INDENT' and inImplicit() |
An INDENT closes an implicit call unless -1. We have seen a CONTROL argument on the line. -2. The last token before the indent is part of the list below | if prevTag not in ['=>', '->', '[', '(', ',', '{', 'TRY', 'ELSE', '=']
- endImplicitCall() while inImplicitCall()
- stack.pop() if inImplicitControl()
- stack.push [tag, i]
- return forward(1) |
Straightforward start of explicit expression | if tag in EXPRESSION_START
- stack.push [tag, i]
- return forward(1) |
Close all implicit expressions inside of explicitly closed expressions. | if tag in EXPRESSION_END
- while inImplicit()
- if inImplicitCall()
- endImplicitCall()
- else if inImplicitObject()
- endImplicitObject()
- else
- stack.pop()
- stack.pop() |
Recognize standard implicit calls like -f a, f() b, f? c, h[0] d etc. | if (tag in IMPLICIT_FUNC and token.spaced or
- tag is '?' and i > 0 and not tokens[i - 1].spaced) and
- (nextTag in IMPLICIT_CALL or
- nextTag in IMPLICIT_UNSPACED_CALL and
- not tokens[i + 1]?.spaced and not tokens[i + 1]?.newLine)
- tag = token[0] = 'FUNC_EXIST' if tag is '?'
- startImplicitCall i + 1
- return forward(2) |
Implicit call taking an implicit indented object as first argument. -f +containing balanced expression. + + + + + findTagsBackwards: (i, tags) ->
+ backStack = []
+ while i >= 0 and (backStack.length or
+ @tag(i) not in tags and
+ (@tag(i) not in EXPRESSION_START or @tokens[i].generated) and
+ @tag(i) not in LINEBREAKS)
+ backStack.push @tag(i) if @tag(i) in EXPRESSION_END
+ backStack.pop() if @tag(i) in EXPRESSION_START and backStack.length
+ i -= 1
+ @tag(i) in tags
+
+
+
+
+ ¶
+
+ Look for signs of implicit calls and objects in the token stream and +add them. + + + addImplicitBracesAndParens: ->
+
+
+
+
+ ¶
+
+ Track current balancing depth (both implicit and explicit) on stack. + + + stack = []
+
+ @scanTokens (token, i, tokens) ->
+ [tag] = token
+ [prevTag] = if i > 0 then tokens[i - 1] else []
+ [nextTag] = if i < tokens.length - 1 then tokens[i + 1] else []
+ stackTop = -> stack[stack.length - 1]
+ startIdx = i
+
+
+
+
+ ¶
+
+ Helper function, used for keeping track of the number of tokens consumed +and spliced, when returning for getting a new token. + + + forward = (n) -> i - startIdx + n
+
+
+
+
+ ¶
+
+ Helper functions + + + inImplicit = -> stackTop()?[2]?.ours
+ inImplicitCall = -> inImplicit() and stackTop()?[0] is '('
+ inImplicitObject = -> inImplicit() and stackTop()?[0] is '{'
+
+
+
+
+ ¶
+
+ Unclosed control statement inside implicit parens (like +class declaration or if-conditionals) + + + inImplicitControl = -> inImplicit and stackTop()?[0] is 'CONTROL'
+
+ startImplicitCall = (j) ->
+ idx = j ? i
+ stack.push ['(', idx, ours: yes]
+ tokens.splice idx, 0, generate 'CALL_START', '('
+ i += 1 if not j?
+
+ endImplicitCall = ->
+ stack.pop()
+ tokens.splice i, 0, generate 'CALL_END', ')'
+ i += 1
+
+ startImplicitObject = (j, startsLine = yes) ->
+ idx = j ? i
+ stack.push ['{', idx, sameLine: yes, startsLine: startsLine, ours: yes]
+ tokens.splice idx, 0, generate '{', generate(new String('{'))
+ i += 1 if not j?
+
+ endImplicitObject = (j) ->
+ j = j ? i
+ stack.pop()
+ tokens.splice j, 0, generate '}', '}'
+ i += 1
+
+
+
+
+ ¶
+
+ Don't end an implicit call on next indent if any of these are in an argument + + + if inImplicitCall() and tag in ['IF', 'TRY', 'FINALLY', 'CATCH',
+ 'CLASS', 'SWITCH']
+ stack.push ['CONTROL', i, ours: true]
+ return forward(1)
+
+ if tag is 'INDENT' and inImplicit()
+
+
+
+
+ ¶
+
+ An
if prevTag not in ['=>', '->', '[', '(', ',', '{', 'TRY', 'ELSE', '=']
+ endImplicitCall() while inImplicitCall()
+ stack.pop() if inImplicitControl()
+ stack.push [tag, i]
+ return forward(1)
+
+
+
+
+ ¶
+
+ Straightforward start of explicit expression + + + if tag in EXPRESSION_START
+ stack.push [tag, i]
+ return forward(1)
+
+
+
+
+ ¶
+
+ Close all implicit expressions inside of explicitly closed expressions. + + + if tag in EXPRESSION_END
+ while inImplicit()
+ if inImplicitCall()
+ endImplicitCall()
+ else if inImplicitObject()
+ endImplicitObject()
+ else
+ stack.pop()
+ stack.pop()
+
+
+
+
+ ¶
+
+ Recognize standard implicit calls like +f a, f() b, f? c, h[0] d etc. + + + if (tag in IMPLICIT_FUNC and token.spaced and not token.stringEnd or
+ tag is '?' and i > 0 and not tokens[i - 1].spaced) and
+ (nextTag in IMPLICIT_CALL or
+ nextTag in IMPLICIT_UNSPACED_CALL and
+ not tokens[i + 1]?.spaced and not tokens[i + 1]?.newLine)
+ tag = token[0] = 'FUNC_EXIST' if tag is '?'
+ startImplicitCall i + 1
+ return forward(2)
+
+
+ ¶
+
+ Implicit call taking an implicit indented object as first argument. + + +
+and + + +
+Don't accept implicit calls of this type, when on the same line as the control strucutures below as that may misinterpret constructs like: -if f - a: 1 -as -if f(a: 1) -which is probably always unintended. -Furthermore don't allow this in literal arrays, as -that creates grammatical ambiguities. | if @matchTags(i, IMPLICIT_FUNC, 'INDENT', null, ':') and
- not @findTagsBackwards(i, ['CLASS', 'EXTENDS', 'IF', 'CATCH',
- 'SWITCH', 'LEADING_WHEN', 'FOR', 'WHILE', 'UNTIL'])
- startImplicitCall i + 1
- stack.push ['INDENT', i + 2]
- return forward(3) |
Implicit objects start here | if tag is ':' |
Go back to the (implicit) start of the object | if @tag(i - 2) is '@' then s = i - 2 else s = i - 1
- s -= 2 while @tag(s - 2) is 'HERECOMMENT'
-
- startsLine = s is 0 or @tag(s - 1) in LINEBREAKS or tokens[s - 1].newLine |
Are we just continuing an already declared object? | if stackTop()
- [stackTag, stackIdx] = stackTop()
- if (stackTag is '{' or stackTag is 'INDENT' and @tag(stackIdx - 1) is '{') and
- (startsLine or @tag(s - 1) is ',' or @tag(s - 1) is '{')
- return forward(1)
-
- startImplicitObject(s, !!startsLine)
- return forward(2) |
End implicit calls when chaining method calls + + +
+as + + +
+which is probably always unintended. +Furthermore don't allow this in literal arrays, as +that creates grammatical ambiguities. + + + + + if tag in IMPLICIT_FUNC and @matchTags(i + 1, 'INDENT', null, ':') and
+ not @findTagsBackwards(i, ['CLASS', 'EXTENDS', 'IF', 'CATCH',
+ 'SWITCH', 'LEADING_WHEN', 'FOR', 'WHILE', 'UNTIL'])
+ startImplicitCall i + 1
+ stack.push ['INDENT', i + 2]
+ return forward(3)
+
+
+
+
+ ¶
+
+ Implicit objects start here + + + if tag is ':'
+
+
+
+
+ ¶
+
+ Go back to the (implicit) start of the object + + + if @tag(i - 2) is '@' then s = i - 2 else s = i - 1
+ s -= 2 while @tag(s - 2) is 'HERECOMMENT'
+
+ startsLine = s is 0 or @tag(s - 1) in LINEBREAKS or tokens[s - 1].newLine
+
+
+
+
+ ¶
+
+ Are we just continuing an already declared object? + + + if stackTop()
+ [stackTag, stackIdx] = stackTop()
+ if (stackTag is '{' or stackTag is 'INDENT' and @tag(stackIdx - 1) is '{') and
+ (startsLine or @tag(s - 1) is ',' or @tag(s - 1) is '{')
+ return forward(1)
+
+ startImplicitObject(s, !!startsLine)
+ return forward(2)
+
+
+ ¶
+
+ End implicit calls when chaining method calls like e.g.: -f -> + + +
| if prevTag is 'OUTDENT' and inImplicitCall() and tag in ['.', '?.', '::', '?::']
- endImplicitCall()
- return forward(1)
-
- stackTop()[2].sameLine = no if inImplicitObject() and tag in LINEBREAKS
-
- if tag in IMPLICIT_END
- while inImplicit()
- [stackTag, stackIdx, {sameLine, startsLine}] = stackTop() |
Close implicit calls when reached end of argument list | if inImplicitCall() and prevTag isnt ','
- endImplicitCall() |
Close implicit objects such as: -return a: 1, b: 2 unless true | else if inImplicitObject() and sameLine and not startsLine
- endImplicitObject() |
Close implicit objects when at end of line, line didn't end with a comma -and the implicit object didn't start the line or the next line doesn't look like -the continuation of an object. | else if inImplicitObject() and tag is 'TERMINATOR' and prevTag isnt ',' and
- not (startsLine and @looksObjectish(i + 1))
- endImplicitObject()
- else
- break |
Close implicit object if comma is the last character -and what comes after doesn't look like it belongs. +.h a + + + + if prevTag is 'OUTDENT' and inImplicitCall() and tag in ['.', '?.', '::', '?::']
+ endImplicitCall()
+ return forward(1)
+
+ stackTop()[2].sameLine = no if inImplicitObject() and tag in LINEBREAKS
+
+ if tag in IMPLICIT_END
+ while inImplicit()
+ [stackTag, stackIdx, {sameLine, startsLine}] = stackTop()
+
+
+
+
+ ¶
+
+ Close implicit calls when reached end of argument list + + + if inImplicitCall() and prevTag isnt ','
+ endImplicitCall()
+
+
+
+
+ ¶
+
+ Close implicit objects such as: +return a: 1, b: 2 unless true + + + else if inImplicitObject() and sameLine and not startsLine
+ endImplicitObject()
+
+
+
+
+ ¶
+
+ Close implicit objects when at end of line, line didn't end with a comma +and the implicit object didn't start the line or the next line doesn't look like +the continuation of an object. + + + else if inImplicitObject() and tag is 'TERMINATOR' and prevTag isnt ',' and
+ not (startsLine and @looksObjectish(i + 1))
+ endImplicitObject()
+ else
+ break
+
+
+ ¶
+
+ Close implicit object if comma is the last character +and what comes after doesn't look like it belongs. This is used for trailing commas and calls, like: -x = + + +
| if tag is ',' and not @looksObjectish(i + 1) and inImplicitObject() and
- (nextTag isnt 'TERMINATOR' or not @looksObjectish(i + 2)) |
When nextTag is OUTDENT the comma is insignificant and -should just be ignored so embed it in the implicit object. - -When it isn't the comma go on to play a role in a call or -array further up the stack, so give it a chance. | offset = if nextTag is 'OUTDENT' then 1 else 0
- while inImplicitObject()
- endImplicitObject i + offset
- return forward(1) |
Add location data to all tokens generated by the rewriter. | addLocationDataToGeneratedTokens: ->
- @scanTokens (token, i, tokens) ->
- return 1 if token[2]
- return 1 unless token.generated or token.explicit
- {last_line, last_column} = tokens[i - 1]?[2] ? last_line: 0, last_column: 0
- token[2] =
- first_line: last_line
- first_column: last_column
- last_line: last_line
- last_column: last_column
- 1 |
Because our grammar is LALR(1), it can't handle some single-line +e = 2 + and + + +
+
+
+
+ if tag is ',' and not @looksObjectish(i + 1) and inImplicitObject() and
+ (nextTag isnt 'TERMINATOR' or not @looksObjectish(i + 2))
+
+
+
+
+ ¶
+
+ When nextTag is OUTDENT the comma is insignificant and +should just be ignored so embed it in the implicit object. + + +When it isn't the comma go on to play a role in a call or +array further up the stack, so give it a chance. + + +
+ offset = if nextTag is 'OUTDENT' then 1 else 0
+ while inImplicitObject()
+ endImplicitObject i + offset
+ return forward(1)
+
+
+
+
+ ¶
+
+ Add location data to all tokens generated by the rewriter. + + + addLocationDataToGeneratedTokens: ->
+ @scanTokens (token, i, tokens) ->
+ return 1 if token[2]
+ return 1 unless token.generated or token.explicit
+ if token[0] is '{' and nextLocation=tokens[i + 1]?[2]
+ {first_line: line, first_column: column} = nextLocation
+ else if prevLocation = tokens[i - 1]?[2]
+ {last_line: line, last_column: column} = prevLocation
+ else
+ line = column = 0
+ token[2] =
+ first_line: line
+ first_column: column
+ last_line: line
+ last_column: column
+ return 1
+
+
+ ¶
+
+ Because our grammar is LALR(1), it can't handle some single-line expressions that lack ending delimiters. The Rewriter adds the implicit -blocks, so it doesn't need to. ')' can close a single-line block, -but we need to make sure it's balanced. | addImplicitIndentation: ->
- starter = indent = outdent = null
-
- condition = (token, i) ->
- token[1] isnt ';' and token[0] in SINGLE_CLOSERS and
- not (token[0] is 'ELSE' and starter not in ['IF', 'THEN'])
-
- action = (token, i) ->
- @tokens.splice (if @tag(i - 1) is ',' then i - 1 else i), 0, outdent
-
- @scanTokens (token, i, tokens) ->
- [tag] = token
- if tag is 'TERMINATOR' and @tag(i + 1) is 'THEN'
- tokens.splice i, 1
- return 0
- if tag is 'ELSE' and @tag(i - 1) isnt 'OUTDENT'
- tokens.splice i, 0, @indentation(token)...
- return 2
- if tag is 'CATCH' and @tag(i + 2) in ['OUTDENT', 'TERMINATOR', 'FINALLY']
- tokens.splice i + 2, 0, @indentation(token)...
- return 4
- if tag in SINGLE_LINERS and @tag(i + 1) isnt 'INDENT' and
- not (tag is 'ELSE' and @tag(i + 1) is 'IF')
- starter = tag
- [indent, outdent] = @indentation token, yes
- indent.fromThen = true if starter is 'THEN'
- tokens.splice i + 1, 0, indent
- @detectEnd i + 2, condition, action
- tokens.splice i, 1 if tag is 'THEN'
- return 1
- return 1 |
Tag postfix conditionals as such, so that we can parse them with a -different precedence. | tagPostfixConditionals: ->
-
- original = null
-
- condition = (token, i) ->
- token[0] in ['TERMINATOR', 'INDENT']
-
- action = (token, i) ->
- if token[0] isnt 'INDENT' or (token.generated and not token.fromThen)
- original[0] = 'POST_' + original[0]
-
- @scanTokens (token, i) ->
- return 1 unless token[0] is 'IF'
- original = token
- @detectEnd i + 1, condition, action
- 1 |
Generate the indentation tokens, based on another token on the same line. | indentation: (token, implicit = no) ->
- indent = ['INDENT', 2]
- outdent = ['OUTDENT', 2]
- indent.generated = outdent.generated = yes if implicit
- indent.explicit = outdent.explicit = yes if not implicit
- [indent, outdent]
-
- generate: generate |
Look up a tag by token index. | tag: (i) -> @tokens[i]?[0] |
Constants | |
List of the token pairs that must be balanced. | BALANCED_PAIRS = [
- ['(', ')']
- ['[', ']']
- ['{', '}']
- ['INDENT', 'OUTDENT'],
- ['CALL_START', 'CALL_END']
- ['PARAM_START', 'PARAM_END']
- ['INDEX_START', 'INDEX_END']
-] |
The inverse mappings of | exports.INVERSES = INVERSES = {} |
The tokens that signal the start/end of a balanced pair. | EXPRESSION_START = []
-EXPRESSION_END = []
-
-for [left, rite] in BALANCED_PAIRS
- EXPRESSION_START.push INVERSES[rite] = left
- EXPRESSION_END .push INVERSES[left] = rite |
Tokens that indicate the close of a clause of an expression. | EXPRESSION_CLOSE = ['CATCH', 'WHEN', 'ELSE', 'FINALLY'].concat EXPRESSION_END |
Tokens that, if followed by an | IMPLICIT_FUNC = ['IDENTIFIER', 'SUPER', ')', 'CALL_END', ']', 'INDEX_END', '@', 'THIS'] |
If preceded by an | IMPLICIT_CALL = [
- 'IDENTIFIER', 'NUMBER', 'STRING', 'JS', 'REGEX', 'NEW', 'PARAM_START', 'CLASS'
- 'IF', 'TRY', 'SWITCH', 'THIS', 'BOOL', 'NULL', 'UNDEFINED', 'UNARY', 'SUPER'
- '@', '->', '=>', '[', '(', '{', '--', '++'
-]
-
-IMPLICIT_UNSPACED_CALL = ['+', '-'] |
Tokens indicating that the implicit call must enclose a block of expressions. | IMPLICIT_BLOCK = ['->', '=>', '{', '[', ','] |
Tokens that always mark the end of an implicit call for single-liners. | IMPLICIT_END = ['POST_IF', 'FOR', 'WHILE', 'UNTIL', 'WHEN', 'BY',
- 'LOOP', 'TERMINATOR'] |
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 = ['TERMINATOR', 'CATCH', 'FINALLY', 'ELSE', 'OUTDENT', 'LEADING_WHEN'] |
Tokens that end a line. | LINEBREAKS = ['TERMINATOR', 'INDENT', 'OUTDENT']
-
- |
addImplicitIndentation: ->
+ starter = indent = outdent = null
+
+ condition = (token, i) ->
+ token[1] isnt ';' and token[0] in SINGLE_CLOSERS and
+ not (token[0] is 'ELSE' and starter isnt 'THEN') and
+ not (token[0] in ['CATCH', 'FINALLY'] and starter in ['->', '=>'])
+
+ action = (token, i) ->
+ @tokens.splice (if @tag(i - 1) is ',' then i - 1 else i), 0, outdent
+
+ @scanTokens (token, i, tokens) ->
+ [tag] = token
+ if tag is 'TERMINATOR' and @tag(i + 1) is 'THEN'
+ tokens.splice i, 1
+ return 0
+ if tag is 'ELSE' and @tag(i - 1) isnt 'OUTDENT'
+ tokens.splice i, 0, @indentation()...
+ return 2
+ if tag is 'CATCH'
+ for j in [1..2] when @tag(i + j) in ['OUTDENT', 'TERMINATOR', 'FINALLY']
+ tokens.splice i + j, 0, @indentation()...
+ return 2 + j
+ if tag in SINGLE_LINERS and @tag(i + 1) isnt 'INDENT' and
+ not (tag is 'ELSE' and @tag(i + 1) is 'IF')
+ starter = tag
+ [indent, outdent] = @indentation yes
+ indent.fromThen = true if starter is 'THEN'
+ tokens.splice i + 1, 0, indent
+ @detectEnd i + 2, condition, action
+ tokens.splice i, 1 if tag is 'THEN'
+ return 1
+ return 1
Tag postfix conditionals as such, so that we can parse them with a +different precedence. +
+ + tagPostfixConditionals: ->
+
+ original = null
+
+ condition = (token, i) ->
+ [tag] = token
+ [prevTag] = @tokens[i - 1]
+ tag is 'TERMINATOR' or (tag is 'INDENT' and prevTag not in SINGLE_LINERS)
+
+ action = (token, i) ->
+ if token[0] isnt 'INDENT' or (token.generated and not token.fromThen)
+ original[0] = 'POST_' + original[0]
+
+ @scanTokens (token, i) ->
+ return 1 unless token[0] is 'IF'
+ original = token
+ @detectEnd i + 1, condition, action
+ return 1
Generate the indentation tokens, based on another token on the same line. +
+ + indentation: (implicit = no) ->
+ indent = ['INDENT', 2]
+ outdent = ['OUTDENT', 2]
+ indent.generated = outdent.generated = yes if implicit
+ indent.explicit = outdent.explicit = yes if not implicit
+ [indent, outdent]
+
+ generate: generate
Look up a tag by token index. +
+ + tag: (i) -> @tokens[i]?[0]
List of the token pairs that must be balanced. +
+ +BALANCED_PAIRS = [
+ ['(', ')']
+ ['[', ']']
+ ['{', '}']
+ ['INDENT', 'OUTDENT'],
+ ['CALL_START', 'CALL_END']
+ ['PARAM_START', 'PARAM_END']
+ ['INDEX_START', 'INDEX_END']
+]
The inverse mappings of BALANCED_PAIRS
we're trying to fix up, so we can
+look things up from either end.
+
exports.INVERSES = INVERSES = {}
The tokens that signal the start/end of a balanced pair. +
+ +EXPRESSION_START = []
+EXPRESSION_END = []
+
+for [left, rite] in BALANCED_PAIRS
+ EXPRESSION_START.push INVERSES[rite] = left
+ EXPRESSION_END .push INVERSES[left] = rite
Tokens that indicate the close of a clause of an expression. +
+ +EXPRESSION_CLOSE = ['CATCH', 'WHEN', 'ELSE', 'FINALLY'].concat EXPRESSION_END
Tokens that, if followed by an IMPLICIT_CALL
, indicate a function invocation.
+
IMPLICIT_FUNC = ['IDENTIFIER', 'SUPER', ')', 'CALL_END', ']', 'INDEX_END', '@', 'THIS']
If preceded by an IMPLICIT_FUNC
, indicates a function invocation.
+
IMPLICIT_CALL = [
+ 'IDENTIFIER', 'NUMBER', 'STRING', 'JS', 'REGEX', 'NEW', 'PARAM_START', 'CLASS'
+ 'IF', 'TRY', 'SWITCH', 'THIS', 'BOOL', 'NULL', 'UNDEFINED', 'UNARY', 'SUPER'
+ 'THROW', '@', '->', '=>', '[', '(', '{', '--', '++'
+]
+
+IMPLICIT_UNSPACED_CALL = ['+', '-']
Tokens that always mark the end of an implicit call for single-liners. +
+ +IMPLICIT_END = ['POST_IF', 'FOR', 'WHILE', 'UNTIL', 'WHEN', 'BY',
+ 'LOOP', 'TERMINATOR']
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 = ['TERMINATOR', 'CATCH', 'FINALLY', 'ELSE', 'OUTDENT', 'LEADING_WHEN']
Tokens that end a line. +
+ +LINEBREAKS = ['TERMINATOR', 'INDENT', 'OUTDENT']
scope.coffee | |
---|---|
The Scope class regulates lexical scoping within CoffeeScript. As you + + + + +
+
+
+
| |
Import the helpers we plan to use. | {extend, last} = require './helpers'
+with external scopes.
+
+
+
+
+
+
+
+ ¶
+
+ Import the helpers we plan to use. + + +
+{extend, last} = require './helpers'
-exports.Scope = class Scope |
The top-level Scope object. | @root: null |
Initialize a scope with its parent, for lookups up the chain, +exports.Scope = class Scope + + + + +
+
+
+
+
+ ¶
+
+ The
+ @root: null
+
+
+ ¶
+
+ Initialize a scope with its parent, for lookups up the chain, as well as a reference to the Block node it belongs to, which is where it should declare its variables, and a reference to the function that -it wraps. | constructor: (@parent, @expressions, @method) ->
- @variables = [{name: 'arguments', type: 'arguments'}]
- @positions = {}
- Scope.root = this unless @parent |
Adds a new variable or overrides an existing one. | add: (name, type, immediate) ->
- return @parent.add name, type, immediate if @shared and not immediate
- if Object::hasOwnProperty.call @positions, name
- @variables[@positions[name]].type = type
- else
- @positions[name] = @variables.push({name, type}) - 1 |
When | namedMethod: ->
- return @method if @method.name or !@parent
- @parent.namedMethod() |
Look up a variable name in lexical scope, and declare it if it does not -already exist. | find: (name) ->
- return yes if @check name
- @add name, 'var'
- no |
Reserve a variable name as originating from a function parameter for this
-scope. No | parameter: (name) ->
- return if @shared and @parent.check name, yes
- @add name, 'param' |
Just check to see if a variable has already been declared, without reserving, -walks up to the root scope. | check: (name) ->
- !!(@type(name) or @parent?.check(name)) |
Generate a temporary variable name at the given index. | temporary: (name, index) ->
- if name.length > 1
- '_' + name + if index > 1 then index - 1 else ''
- else
- '_' + (index + parseInt name, 36).toString(36).replace /\d/g, 'a' |
Gets the type of a variable. | type: (name) ->
- return v.type for v in @variables when v.name is name
- null |
If we need to store an intermediate result, find an available name for a
-compiler-generated variable. | freeVariable: (name, reserve=true) ->
- index = 0
- index++ while @check((temp = @temporary name, index))
- @add temp, 'var', yes if reserve
- temp |
Ensure that an assignment is made at the top of this scope -(or at the top-level scope, if requested). | assign: (name, value) ->
- @add name, {value, assigned: yes}, yes
- @hasAssignments = yes |
Does this scope have any declared variables? | hasDeclarations: ->
- !!@declaredVariables().length |
Return the list of variables first declared in this scope. | declaredVariables: ->
- realVars = []
- tempVars = []
- for v in @variables when v.type is 'var'
- (if v.name.charAt(0) is '_' then tempVars else realVars).push v.name
- realVars.sort().concat tempVars.sort() |
Return the list of assignments that are supposed to be made at the top -of this scope. | assignedVariables: ->
- "#{v.name} = #{v.type.value}" for v in @variables when v.type.assigned
-
- |
+ constructor: (@parent, @expressions, @method) ->
+ @variables = [{name: 'arguments', type: 'arguments'}]
+ @positions = {}
+ Scope.root = this unless @parent
Adds a new variable or overrides an existing one. +
+ +
+ add: (name, type, immediate) ->
+ return @parent.add name, type, immediate if @shared and not immediate
+ if Object::hasOwnProperty.call @positions, name
+ @variables[@positions[name]].type = type
+ else
+ @positions[name] = @variables.push({name, type}) - 1
When super
is called, we need to find the name of the current method we're
+in, so that we know how to invoke the same method of the parent class. This
+can get complicated if super is being called from an inner function.
+namedMethod
will walk up the scope tree until it either finds the first
+function object that has a name filled in, or bottoms out.
+
+ namedMethod: ->
+ return @method if @method?.name or !@parent
+ @parent.namedMethod()
Look up a variable name in lexical scope, and declare it if it does not +already exist. +
+ +
+ find: (name) ->
+ return yes if @check name
+ @add name, 'var'
+ no
Reserve a variable name as originating from a function parameter for this
+scope. No var
required for internal references.
+
+ parameter: (name) ->
+ return if @shared and @parent.check name, yes
+ @add name, 'param'
Just check to see if a variable has already been declared, without reserving, +walks up to the root scope. +
+ +
+ check: (name) ->
+ !!(@type(name) or @parent?.check(name))
Generate a temporary variable name at the given index. +
+ +
+ temporary: (name, index) ->
+ if name.length > 1
+ '_' + name + if index > 1 then index - 1 else ''
+ else
+ '_' + (index + parseInt name, 36).toString(36).replace /\d/g, 'a'
Gets the type of a variable. +
+ +
+ type: (name) ->
+ return v.type for v in @variables when v.name is name
+ null
If we need to store an intermediate result, find an available name for a
+compiler-generated variable. _var
, _var2
, and so on...
+
+ freeVariable: (name, reserve=true) ->
+ index = 0
+ index++ while @check((temp = @temporary name, index))
+ @add temp, 'var', yes if reserve
+ temp
Ensure that an assignment is made at the top of this scope +(or at the top-level scope, if requested). +
+ +
+ assign: (name, value) ->
+ @add name, {value, assigned: yes}, yes
+ @hasAssignments = yes
Does this scope have any declared variables? +
+ +
+ hasDeclarations: ->
+ !!@declaredVariables().length
Return the list of variables first declared in this scope. +
+ +
+ declaredVariables: ->
+ realVars = []
+ tempVars = []
+ for v in @variables when v.type is 'var'
+ (if v.name.charAt(0) is '_' then tempVars else realVars).push v.name
+ realVars.sort().concat tempVars.sort()
Return the list of assignments that are supposed to be made at the top +of this scope. +
+ +
+ assignedVariables: ->
+ "#{v.name} = #{v.type.value}" for v in @variables when v.type.assigned
sourcemap.coffee | |
---|---|
LineMapping | |
Hold data about mappings for one line of generated source code. | class LineMapping
- constructor: (@generatedLine) -> |
columnMap keeps track of which columns we've already mapped. | @columnMap = {} |
columnMappings is an array of all column mappings, sorted by generated-column. | @columnMappings = []
-
- addMapping: (generatedColumn, [sourceLine, sourceColumn], options={}) ->
- if @columnMap[generatedColumn] and options.noReplace |
We already have a mapping for this column. | return
-
- @columnMap[generatedColumn] = {
- generatedLine: @generatedLine
- generatedColumn
- sourceLine
- sourceColumn
- }
-
- @columnMappings.push @columnMap[generatedColumn]
- @columnMappings.sort (a,b) -> a.generatedColumn - b.generatedColumn
-
- getSourcePosition: (generatedColumn) ->
- answer = null
- lastColumnMapping = null
- for columnMapping in @columnMappings
- if columnMapping.generatedColumn > generatedColumn
- break
- else
- lastColumnMapping = columnMapping
- if lastColumnMapping
- answer = [lastColumnMapping.sourceLine, lastColumnMapping.sourceColumn] |
SourceMap | |
Maps locations in a generated source file back to locations in the original source file. - -This is intentionally agnostic towards how a source map might be represented on disk. A
-SourceMap can be converted to a "v3" style sourcemap with | class exports.SourceMap
- constructor: () -> |
| @generatedLines = [] |
Adds a mapping to this SourceMap. - -
If | addMapping: (sourceLocation, generatedLocation, options={}) ->
- [generatedLine, generatedColumn] = generatedLocation
-
- lineMapping = @generatedLines[generatedLine]
- if not lineMapping
- lineMapping = @generatedLines[generatedLine] = new LineMapping(generatedLine)
-
- lineMapping.addMapping generatedColumn, sourceLocation, options |
Returns [sourceLine, sourceColumn], or null if no mapping could be found. | getSourcePosition: ([generatedLine, generatedColumn]) ->
- answer = null
- lineMapping = @generatedLines[generatedLine]
- if not lineMapping |
TODO: Search backwards for the line? | else
- answer = lineMapping.getSourcePosition generatedColumn
-
- answer |
| forEachMapping: (fn) ->
- for lineMapping, generatedLineNumber in @generatedLines
- if lineMapping
- for columnMapping in lineMapping.columnMappings
- fn(columnMapping) |
generateV3SourceMap | |
Builds a V3 source map from a SourceMap object. -Returns the generated JSON as a string. | exports.generateV3SourceMap = (sourceMap, sourceFile=null, generatedFile=null) ->
- writingGeneratedLine = 0
- lastGeneratedColumnWritten = 0
- lastSourceLineWritten = 0
- lastSourceColumnWritten = 0
- needComma = no
-
- mappings = ""
-
- sourceMap.forEachMapping (mapping) ->
- while writingGeneratedLine < mapping.generatedLine
- lastGeneratedColumnWritten = 0
- needComma = no
- mappings += ";"
- writingGeneratedLine++ |
Write a comma if we've already written a segment on this line. | if needComma
- mappings += ","
- needComma = no |
Write the next segment. -Segments can be 1, 4, or 5 values. If just one, then it is a generated column which -doesn't match anything in the source code. - -Fields are all zero-based, and relative to the previous occurence unless otherwise noted: - * starting-column in generated source, relative to previous occurence for the current line. - * index into the "sources" list - * starting line in the original source - * starting column in the original source - * index into the "names" list associated with this segment. | |
Add the generated start-column | mappings += exports.vlqEncodeValue(mapping.generatedColumn - lastGeneratedColumnWritten)
- lastGeneratedColumnWritten = mapping.generatedColumn |
Add the index into the sources list | mappings += exports.vlqEncodeValue(0) |
Add the source start-line | mappings += exports.vlqEncodeValue(mapping.sourceLine - lastSourceLineWritten)
- lastSourceLineWritten = mapping.sourceLine |
Add the source start-column | mappings += exports.vlqEncodeValue(mapping.sourceColumn - lastSourceColumnWritten)
- lastSourceColumnWritten = mapping.sourceColumn |
TODO: Do we care about symbol names for CoffeeScript? Probably not. | needComma = yes
-
- answer = {
- version: 3
- file: generatedFile
- sourceRoot: ""
- sources: if sourceFile then [sourceFile] else []
- names: []
- mappings
- }
-
- return JSON.stringify answer, null, 2 |
Load a SourceMap from a JSON string. Returns the SourceMap object. | exports.loadV3SourceMap = (sourceMap) ->
- todo() |
Base64 encoding helpers | BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
-MAX_BASE64_VALUE = BASE64_CHARS.length - 1
-
-encodeBase64Char = (value) ->
- if value > MAX_BASE64_VALUE
- throw new Error "Cannot encode value #{value} > #{MAX_BASE64_VALUE}"
- else if value < 0
- throw new Error "Cannot encode value #{value} < 0"
- BASE64_CHARS[value]
-
-decodeBase64Char = (char) ->
- value = BASE64_CHARS.indexOf char
- if value == -1
- throw new Error "Invalid Base 64 character: #{char}"
- value |
Base 64 VLQ encoding/decoding helpers | |
Note that SourceMap VLQ encoding is "backwards". MIDI style VLQ encoding puts the -most-significant-bit (MSB) from the original value into the MSB of the VLQ encoded value -(see http://en.wikipedia.org/wiki/File:Uintvar_coding.svg). SourceMap VLQ does things -the other way around, with the least significat four bits of the original value encoded -into the first byte of the VLQ encoded value. | VLQ_SHIFT = 5
-VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT # 0010 0000
-VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1 # 0001 1111 |
Encode a value as Base 64 VLQ. | exports.vlqEncodeValue = (value) -> |
Least significant bit represents the sign. | signBit = if value < 0 then 1 else 0 |
Next bits are the actual value | valueToEncode = (Math.abs(value) << 1) + signBit
-
- answer = "" |
Make sure we encode at least one character, even if valueToEncode is 0. | while valueToEncode || !answer
- nextVlqChunk = valueToEncode & VLQ_VALUE_MASK
- valueToEncode = valueToEncode >> VLQ_SHIFT
-
- if valueToEncode
- nextVlqChunk |= VLQ_CONTINUATION_BIT
-
- answer += encodeBase64Char(nextVlqChunk)
-
- return answer |
Decode a Base 64 VLQ value. - -Returns | exports.vlqDecodeValue = (str, offset=0) ->
- position = offset
- done = false
-
- value = 0
- continuationShift = 0
-
- while !done
- nextVlqChunk = decodeBase64Char(str[position])
- position += 1
-
- nextChunkValue = nextVlqChunk & VLQ_VALUE_MASK
- value += (nextChunkValue << continuationShift)
-
- if !(nextVlqChunk & VLQ_CONTINUATION_BIT) |
We'll be done after this character. | done = true |
Bits are encoded least-significant first (opposite of MIDI VLQ). Increase the -continuationShift, so the next byte will end up where it should in the value. | continuationShift += VLQ_SHIFT
-
- consumed = position - offset |
Least significant bit represents the sign. | signBit = value & 1
- value = value >> 1
-
- if signBit then value = -value
-
- return [value, consumed]
-
- |
Source maps allow JavaScript runtimes to match running JavaScript back to +the original source code that corresponds to it. This can be minified +JavaScript, but in our case, we're concerned with mapping pretty-printed +JavaScript back to CoffeeScript. +
+ +In order to produce maps, we must keep track of positions (line number, column number) +that originated every node in the syntax tree, and be able to generate a +map file +— which is a compact, VLQ-encoded representation of the JSON serialization +of this information — to write out alongside the generated JavaScript. +
+ +A LineMap object keeps track of information about original line and column +positions for a single line of output JavaScript code. +SourceMaps are implemented in terms of LineMaps. +
+ +
+class LineMap
+ constructor: (@line) ->
+ @columns = []
+
+ add: (column, [sourceLine, sourceColumn], options={}) ->
+ return if @columns[column] and options.noReplace
+ @columns[column] = {line: @line, column, sourceLine, sourceColumn}
+
+ sourceLocation: (column) ->
+ column-- until (mapping = @columns[column]) or (column <= 0)
+ mapping and [mapping.sourceLine, mapping.sourceColumn]
Maps locations in a single generated JavaScript file back to locations in +the original CoffeeScript source file. +
+ +This is intentionally agnostic towards how a source map might be represented on +disk. Once the compiler is ready to produce a "v3"-style source map, we can walk +through the arrays of line and column buffer to produce it. +
+ +
+class SourceMap
+ constructor: ->
+ @lines = []
Adds a mapping to this SourceMap. sourceLocation
and generatedLocation
+are both [line, column]
arrays. If options.noReplace
is true, then if there
+is already a mapping for the specified line
and column
, this will have no
+effect.
+
+ add: (sourceLocation, generatedLocation, options = {}) ->
+ [line, column] = generatedLocation
+ lineMap = (@lines[line] or= new LineMap(line))
+ lineMap.add column, sourceLocation, options
Look up the original position of a given line
and column
in the generated
+code.
+
+ sourceLocation: ([line, column]) ->
+ line-- until (lineMap = @lines[line]) or (line <= 0)
+ lineMap and lineMap.sourceLocation column
Builds up a V3 source map, returning the generated JSON as a string.
+options.sourceRoot
may be used to specify the sourceRoot written to the source
+map. Also, options.sourceFiles
and options.generatedFile
may be passed to
+set "sources" and "file", respectively.
+
+ generate: (options = {}, code = null) ->
+ writingline = 0
+ lastColumn = 0
+ lastSourceLine = 0
+ lastSourceColumn = 0
+ needComma = no
+ buffer = ""
+
+ for lineMap, lineNumber in @lines when lineMap
+ for mapping in lineMap.columns when mapping
+ while writingline < mapping.line
+ lastColumn = 0
+ needComma = no
+ buffer += ";"
+ writingline++
Write a comma if we've already written a segment on this line. +
+ +
+ if needComma
+ buffer += ","
+ needComma = no
Write the next segment. Segments can be 1, 4, or 5 values. If just one, then it +is a generated column which doesn't match anything in the source code. +
+ +The starting column in the generated source, relative to any previous recorded +column for the current line: +
+ +
+ buffer += @encodeVlq mapping.column - lastColumn
+ lastColumn = mapping.column
The index into the list of sources: +
+ +
+ buffer += @encodeVlq 0
The starting line in the original source, relative to the previous source line. +
+ +
+ buffer += @encodeVlq mapping.sourceLine - lastSourceLine
+ lastSourceLine = mapping.sourceLine
The starting column in the original source, relative to the previous column. +
+ +
+ buffer += @encodeVlq mapping.sourceColumn - lastSourceColumn
+ lastSourceColumn = mapping.sourceColumn
+ needComma = yes
Produce the canonical JSON object format for a "v3" source map. +
+ +
+ v3 =
+ version: 3
+ file: options.generatedFile or ''
+ sourceRoot: options.sourceRoot or ''
+ sources: options.sourceFiles or ['']
+ names: []
+ mappings: buffer
+
+ v3.sourcesContent = [code] if options.inline
+
+ JSON.stringify v3, null, 2
Note that SourceMap VLQ encoding is "backwards". MIDI-style VLQ encoding puts +the most-significant-bit (MSB) from the original value into the MSB of the VLQ +encoded value (see Wikipedia). +SourceMap VLQ does things the other way around, with the least significat four +bits of the original value encoded into the first byte of the VLQ encoded value. +
+ +
+ VLQ_SHIFT = 5
+ VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT # 0010 0000
+ VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1 # 0001 1111
+
+ encodeVlq: (value) ->
+ answer = ''
Least significant bit represents the sign. +
+ + signBit = if value < 0 then 1 else 0
The next bits are the actual value. +
+ + valueToEncode = (Math.abs(value) << 1) + signBit
Make sure we encode at least one character, even if valueToEncode is 0. +
+ + while valueToEncode or not answer
+ nextChunk = valueToEncode & VLQ_VALUE_MASK
+ valueToEncode = valueToEncode >> VLQ_SHIFT
+ nextChunk |= VLQ_CONTINUATION_BIT if valueToEncode
+ answer += @encodeBase64 nextChunk
+
+ answer
+ BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+
+ encodeBase64: (value) ->
+ BASE64_CHARS[value] or throw new Error "Cannot Base64 encode value: #{value}"
Our API for source maps is just the SourceMap
class.
+
+module.exports = SourceMap
Latest Version: - 1.6.1 + 1.6.3
@@ -266,16 +264,6 @@ sudo bin/cake installdirectly to stdout. -
-l, --lint
-s, --stdio
-e, --eval
-l, --literate
-r, --require
-e, --eval
+ Destructuring assignment is also useful when combined with class constructors + to assign properties to your instance from an options object passed to the constructor. +
+ <%= code_for('constructor_destructuring', 'contents.join("")') %>@@ -869,6 +863,11 @@ Expressions
<%= code_for('switch') %> ++ Switch statements can also be used without a control expression, turning them in to a cleaner alternative to if/else chains. +
+ <%= code_for('switch_with_no_expression') %> +Try/Catch/Finally @@ -922,7 +921,7 @@ Expressions Block Regular Expressions Similar to block strings and comments, CoffeeScript supports block regexes — extended regular expressions that ignore internal whitespace and can contain - comments and interpolation. Modeled after Perl's /x modifier, CoffeeSctipt's + comments and interpolation. Modeled after Perl's /x modifier, CoffeeScript's block regexes are delimited by /// and go a long way towards making complex regular expressions readable. To quote from the CoffeeScript source:
@@ -1165,6 +1164,13 @@ Expressions The FAQ+ + 1.6.3 + + +
+ + 1.6.2 + + +
1.6.1
@@ -1939,8 +2003,10 @@ Expressions
else
$(el).text window.compiledJS
$('#error').hide()
- catch error
- $('#error').text(error.message).show()
+ catch {location, message}
+ if location?
+ message = "Error on line #{location.first_line + 1}: #{message}"
+ $('#error').text(message).show()
# Update permalink
$('#repl_permalink').attr 'href', "##{sourceFragment}#{encodeURIComponent source}"
diff --git a/documentation/js/aliases.js b/documentation/js/aliases.js
index ee5e636f88..0cbef1c4d9 100644
--- a/documentation/js/aliases.js
+++ b/documentation/js/aliases.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var volume, winner;
if (ignition === true) {
diff --git a/documentation/js/array_comprehensions.js b/documentation/js/array_comprehensions.js
index 94d16539fa..88dbfcc955 100644
--- a/documentation/js/array_comprehensions.js
+++ b/documentation/js/array_comprehensions.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var courses, dish, food, foods, i, _i, _j, _k, _len, _len1, _len2, _ref;
_ref = ['toast', 'cheese', 'wine'];
diff --git a/documentation/js/block_comment.js b/documentation/js/block_comment.js
index 186c70c3d9..5f8d9decd5 100644
--- a/documentation/js/block_comment.js
+++ b/documentation/js/block_comment.js
@@ -1,6 +1,6 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
/*
-CoffeeScript Compiler v1.6.1
+SkinnyMochaHalfCaffScript Compiler v1.0
Released under the MIT License
*/
diff --git a/documentation/js/cake_tasks.js b/documentation/js/cake_tasks.js
index d5c56652d6..43a0f699c9 100644
--- a/documentation/js/cake_tasks.js
+++ b/documentation/js/cake_tasks.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var fs;
fs = require('fs');
diff --git a/documentation/js/classes.js b/documentation/js/classes.js
index 64cd05a9cb..5ee7effe93 100644
--- a/documentation/js/classes.js
+++ b/documentation/js/classes.js
@@ -1,10 +1,9 @@
-// Generated by CoffeeScript 1.6.1
-var Animal, Horse, Snake, sam, tom,
+// Generated by CoffeeScript 1.6.3
+var Animal, Horse, Snake, sam, tom, _ref, _ref1,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Animal = (function() {
-
function Animal(name) {
this.name = name;
}
@@ -18,11 +17,11 @@ Animal = (function() {
})();
Snake = (function(_super) {
-
__extends(Snake, _super);
function Snake() {
- return Snake.__super__.constructor.apply(this, arguments);
+ _ref = Snake.__super__.constructor.apply(this, arguments);
+ return _ref;
}
Snake.prototype.move = function() {
@@ -35,11 +34,11 @@ Snake = (function(_super) {
})(Animal);
Horse = (function(_super) {
-
__extends(Horse, _super);
function Horse() {
- return Horse.__super__.constructor.apply(this, arguments);
+ _ref1 = Horse.__super__.constructor.apply(this, arguments);
+ return _ref1;
}
Horse.prototype.move = function() {
diff --git a/documentation/js/comparisons.js b/documentation/js/comparisons.js
index 6b4a776523..c24b4c564d 100644
--- a/documentation/js/comparisons.js
+++ b/documentation/js/comparisons.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var cholesterol, healthy;
cholesterol = 127;
diff --git a/documentation/js/conditionals.js b/documentation/js/conditionals.js
index c192f2d81b..0c45c43afb 100644
--- a/documentation/js/conditionals.js
+++ b/documentation/js/conditionals.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var date, mood;
if (singing) {
diff --git a/documentation/js/constructor_destructuring.js b/documentation/js/constructor_destructuring.js
new file mode 100644
index 0000000000..13b81030b9
--- /dev/null
+++ b/documentation/js/constructor_destructuring.js
@@ -0,0 +1,11 @@
+// Generated by CoffeeScript 1.6.3
+var Person;
+
+Person = (function() {
+ function Person(options) {
+ this.name = options.name, this.age = options.age, this.height = options.height;
+ }
+
+ return Person;
+
+})();
diff --git a/documentation/js/default_args.js b/documentation/js/default_args.js
index 4228ffdbb8..aab7993b31 100644
--- a/documentation/js/default_args.js
+++ b/documentation/js/default_args.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var fill;
fill = function(container, liquid) {
diff --git a/documentation/js/do.js b/documentation/js/do.js
index 501164cbdb..aa09cc0b17 100644
--- a/documentation/js/do.js
+++ b/documentation/js/do.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var filename, _fn, _i, _len;
_fn = function(filename) {
diff --git a/documentation/js/embedded.js b/documentation/js/embedded.js
index cba575d16b..f914070dbe 100644
--- a/documentation/js/embedded.js
+++ b/documentation/js/embedded.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var hi;
hi = function() {
diff --git a/documentation/js/existence.js b/documentation/js/existence.js
index 34c68bf228..31e2d11ca9 100644
--- a/documentation/js/existence.js
+++ b/documentation/js/existence.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var footprints, solipsism, speed;
if ((typeof mind !== "undefined" && mind !== null) && (typeof world === "undefined" || world === null)) {
diff --git a/documentation/js/expressions.js b/documentation/js/expressions.js
index ed34a10c59..23eedf1217 100644
--- a/documentation/js/expressions.js
+++ b/documentation/js/expressions.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var eldest, grade;
grade = function(student) {
diff --git a/documentation/js/expressions_assignment.js b/documentation/js/expressions_assignment.js
index fea6e30686..fe50ae687b 100644
--- a/documentation/js/expressions_assignment.js
+++ b/documentation/js/expressions_assignment.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var one, six, three, two;
six = (one = 1) + (two = 2) + (three = 3);
diff --git a/documentation/js/expressions_comprehension.js b/documentation/js/expressions_comprehension.js
index 2ffbbcf7e7..0e17a431c9 100644
--- a/documentation/js/expressions_comprehension.js
+++ b/documentation/js/expressions_comprehension.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var globals, name;
globals = ((function() {
diff --git a/documentation/js/expressions_try.js b/documentation/js/expressions_try.js
index 42f4476c52..f0f8534ea5 100644
--- a/documentation/js/expressions_try.js
+++ b/documentation/js/expressions_try.js
@@ -1,9 +1,11 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
+var error;
alert((function() {
try {
return nonexistent / void 0;
- } catch (error) {
+ } catch (_error) {
+ error = _error;
return "And the error is ... " + error;
}
})());
diff --git a/documentation/js/fat_arrow.js b/documentation/js/fat_arrow.js
index 0b0fe2586d..dfa3e1b852 100644
--- a/documentation/js/fat_arrow.js
+++ b/documentation/js/fat_arrow.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var Account;
Account = function(customer, cart) {
diff --git a/documentation/js/functions.js b/documentation/js/functions.js
index a422b1f9ff..00a4db0aa9 100644
--- a/documentation/js/functions.js
+++ b/documentation/js/functions.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var cube, square;
square = function(x) {
diff --git a/documentation/js/heredocs.js b/documentation/js/heredocs.js
index 7877e9edb4..4032044b1e 100644
--- a/documentation/js/heredocs.js
+++ b/documentation/js/heredocs.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var html;
html = "\n cup of coffeescript\n";
diff --git a/documentation/js/heregexes.js b/documentation/js/heregexes.js
index 2d7ff0ab22..dff3cb994b 100644
--- a/documentation/js/heregexes.js
+++ b/documentation/js/heregexes.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var OPERATOR;
OPERATOR = /^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>])\2=?|\?\.|\.{2,3})/;
diff --git a/documentation/js/interpolation.js b/documentation/js/interpolation.js
index 4a0ca18cee..41377c166d 100644
--- a/documentation/js/interpolation.js
+++ b/documentation/js/interpolation.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var author, quote, sentence;
author = "Wittgenstein";
diff --git a/documentation/js/multiple_return_values.js b/documentation/js/multiple_return_values.js
index 5cdcf954a6..28d51e8a95 100644
--- a/documentation/js/multiple_return_values.js
+++ b/documentation/js/multiple_return_values.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var city, forecast, temp, weatherReport, _ref;
weatherReport = function(location) {
diff --git a/documentation/js/object_comprehensions.js b/documentation/js/object_comprehensions.js
index 010a295277..45ab45cf6d 100644
--- a/documentation/js/object_comprehensions.js
+++ b/documentation/js/object_comprehensions.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var age, ages, child, yearsOld;
yearsOld = {
diff --git a/documentation/js/object_extraction.js b/documentation/js/object_extraction.js
index 619de364ad..27c9be44ee 100644
--- a/documentation/js/object_extraction.js
+++ b/documentation/js/object_extraction.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var city, futurists, name, street, _ref, _ref1;
futurists = {
diff --git a/documentation/js/objects_and_arrays.js b/documentation/js/objects_and_arrays.js
index f7f0e89a2f..242bb6bfcd 100644
--- a/documentation/js/objects_and_arrays.js
+++ b/documentation/js/objects_and_arrays.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var bitlist, kids, singers, song;
song = ["do", "re", "mi", "fa", "so"];
diff --git a/documentation/js/objects_reserved.js b/documentation/js/objects_reserved.js
index e4ef251b47..5f80edc5cc 100644
--- a/documentation/js/objects_reserved.js
+++ b/documentation/js/objects_reserved.js
@@ -1,5 +1,4 @@
-// Generated by CoffeeScript 1.6.1
-
+// Generated by CoffeeScript 1.6.3
$('.account').attr({
"class": 'active'
});
diff --git a/documentation/js/overview.js b/documentation/js/overview.js
index cfc977b249..f8b6d494a9 100644
--- a/documentation/js/overview.js
+++ b/documentation/js/overview.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var cubes, list, math, num, number, opposite, race, square,
__slice = [].slice;
diff --git a/documentation/js/parallel_assignment.js b/documentation/js/parallel_assignment.js
index 62e3d94a51..a33a28e8db 100644
--- a/documentation/js/parallel_assignment.js
+++ b/documentation/js/parallel_assignment.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var theBait, theSwitch, _ref;
theBait = 1000;
diff --git a/documentation/js/patterns_and_splats.js b/documentation/js/patterns_and_splats.js
index f471ae4139..f4922ab9ba 100644
--- a/documentation/js/patterns_and_splats.js
+++ b/documentation/js/patterns_and_splats.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var close, contents, open, tag, _i, _ref,
__slice = [].slice;
diff --git a/documentation/js/prototypes.js b/documentation/js/prototypes.js
index 3e60d0c78c..28b9a2fbce 100644
--- a/documentation/js/prototypes.js
+++ b/documentation/js/prototypes.js
@@ -1,5 +1,4 @@
-// Generated by CoffeeScript 1.6.1
-
+// Generated by CoffeeScript 1.6.3
String.prototype.dasherize = function() {
return this.replace(/_/g, "-");
};
diff --git a/documentation/js/range_comprehensions.js b/documentation/js/range_comprehensions.js
index 684c4d1623..8c8796844e 100644
--- a/documentation/js/range_comprehensions.js
+++ b/documentation/js/range_comprehensions.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var countdown, num;
countdown = (function() {
diff --git a/documentation/js/scope.js b/documentation/js/scope.js
index ae0194e15b..5404dc8fc7 100644
--- a/documentation/js/scope.js
+++ b/documentation/js/scope.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var changeNumbers, inner, outer;
outer = 1;
diff --git a/documentation/js/slices.js b/documentation/js/slices.js
index 32d26880b9..c7e4498536 100644
--- a/documentation/js/slices.js
+++ b/documentation/js/slices.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var copy, end, middle, numbers, start;
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
diff --git a/documentation/js/soaks.js b/documentation/js/soaks.js
index 1c0415a72b..81317881e5 100644
--- a/documentation/js/soaks.js
+++ b/documentation/js/soaks.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var zip, _ref;
zip = typeof lottery.drawWinner === "function" ? (_ref = lottery.drawWinner().address) != null ? _ref.zipcode : void 0 : void 0;
diff --git a/documentation/js/splats.js b/documentation/js/splats.js
index 0f72d97923..1cded35024 100644
--- a/documentation/js/splats.js
+++ b/documentation/js/splats.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var awardMedals, contenders, gold, rest, silver,
__slice = [].slice;
diff --git a/documentation/js/splices.js b/documentation/js/splices.js
index 0d4d0cdf8f..a53ba7704e 100644
--- a/documentation/js/splices.js
+++ b/documentation/js/splices.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var numbers, _ref;
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
diff --git a/documentation/js/strings.js b/documentation/js/strings.js
index 7ba6f2ae26..61de561a10 100644
--- a/documentation/js/strings.js
+++ b/documentation/js/strings.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var mobyDick;
mobyDick = "Call me Ishmael. Some years ago -- never mind how long precisely -- having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world...";
diff --git a/documentation/js/switch.js b/documentation/js/switch.js
index 4f9f0baa3c..83500b224b 100644
--- a/documentation/js/switch.js
+++ b/documentation/js/switch.js
@@ -1,5 +1,4 @@
-// Generated by CoffeeScript 1.6.1
-
+// Generated by CoffeeScript 1.6.3
switch (day) {
case "Mon":
go(work);
diff --git a/documentation/js/switch_with_no_expression.js b/documentation/js/switch_with_no_expression.js
new file mode 100644
index 0000000000..f92c7af5ba
--- /dev/null
+++ b/documentation/js/switch_with_no_expression.js
@@ -0,0 +1,19 @@
+// Generated by CoffeeScript 1.6.3
+var grade, score;
+
+score = 76;
+
+grade = (function() {
+ switch (false) {
+ case !(score < 60):
+ return 'F';
+ case !(score < 70):
+ return 'D';
+ case !(score < 80):
+ return 'C';
+ case !(score < 90):
+ return 'B';
+ default:
+ return 'A';
+ }
+})();
diff --git a/documentation/js/try.js b/documentation/js/try.js
index 33c56667d2..caf3ae56a7 100644
--- a/documentation/js/try.js
+++ b/documentation/js/try.js
@@ -1,9 +1,11 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
+var error;
try {
allHellBreaksLoose();
catsAndDogsLivingTogether();
-} catch (error) {
+} catch (_error) {
+ error = _error;
print(error);
} finally {
cleanUp();
diff --git a/documentation/js/while.js b/documentation/js/while.js
index bcf82cb76a..a5238abd5c 100644
--- a/documentation/js/while.js
+++ b/documentation/js/while.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
var lyrics, num;
if (this.studyingEconomics) {
diff --git a/extras/coffee-script.js b/extras/coffee-script.js
index 1e124b343c..f8263a7f96 100644
--- a/extras/coffee-script.js
+++ b/extras/coffee-script.js
@@ -1,11 +1,12 @@
/**
- * CoffeeScript Compiler v1.6.1
+ * CoffeeScript Compiler v1.6.3
* http://coffeescript.org
*
* Copyright 2011, Jeremy Ashkenas
* Released under the MIT License
*/
-(function(root){var CoffeeScript=function(){function require(e){return require[e]}return require["./helpers"]=new function(){var e=this;(function(){var t,n,i,s;e.starts=function(e,t,n){return t===e.substr(n,t.length)},e.ends=function(e,t,n){var i;return i=t.length,t===e.substr(e.length-i-(n||0),i)},e.compact=function(e){var t,n,i,s;for(s=[],n=0,i=e.length;i>n;n++)t=e[n],t&&s.push(t);return s},e.count=function(e,t){var n,i;if(n=i=0,!t.length)return 1/0;for(;i=1+e.indexOf(t,i);)n++;return n},e.merge=function(e,t){return n(n({},e),t)},n=e.extend=function(e,t){var n,i;for(n in t)i=t[n],e[n]=i;return e},e.flatten=i=function(e){var t,n,s,r;for(n=[],s=0,r=e.length;r>s;s++)t=e[s],t instanceof Array?n=n.concat(i(t)):n.push(t);return n},e.del=function(e,t){var n;return n=e[t],delete e[t],n},e.last=function(e,t){return e[e.length-(t||0)-1]},e.some=null!=(s=Array.prototype.some)?s:function(e){var t,n,i;for(n=0,i=this.length;i>n;n++)if(t=this[n],e(t))return!0;return!1},t=function(e,t){return t?{first_line:e.first_line,first_column:e.first_column,last_line:t.last_line,last_column:t.last_column}:e},e.addLocationDataFn=function(e,n){return function(i){return"object"==typeof i&&i.updateLocationDataIfMissing&&i.updateLocationDataIfMissing(t(e,n)),i}},e.locationDataToString=function(e){var t;return"2"in e&&"first_line"in e[2]?t=e[2]:"first_line"in e&&(t=e),t?""+(t.first_line+1)+":"+(t.first_column+1)+"-"+(""+(t.last_line+1)+":"+(t.last_column+1)):"No location data"},e.baseFileName=function(e,t){var n;return null==t&&(t=!1),n=e.split("/"),e=n[n.length-1],t?(n=e.split("."),n.pop(),"coffee"===n[n.length-1]&&n.pop(),n.join(".")):e},e.isCoffee=function(e){return/\.((lit)?coffee|coffee\.md)$/.test(e)},e.isLiterate=function(e){return/\.(litcoffee|coffee\.md)$/.test(e)}}).call(this)},require["./rewriter"]=new function(){var e=this;(function(){var t,n,i,s,r,a,o,c,h,l,u,p,d,f,m,g,b,k,y,v=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1},w=[].slice;for(f=function(e,t){var n;return n=[e,t],n.generated=!0,n},e.Rewriter=function(){function e(){}return e.prototype.rewrite=function(e){return this.tokens=e,this.removeLeadingNewlines(),this.removeMidExpressionNewlines(),this.closeOpenCalls(),this.closeOpenIndexes(),this.addImplicitIndentation(),this.tagPostfixConditionals(),this.addImplicitBracesAndParens(),this.addLocationDataToGeneratedTokens(),this.tokens},e.prototype.scanTokens=function(e){var t,n,i;for(i=this.tokens,t=0;n=i[t];)t+=e.call(this,n,t,i);return!0},e.prototype.detectEnd=function(e,t,n){var r,a,o,c,h;for(o=this.tokens,r=0;a=o[e];){if(0===r&&t.call(this,a,e))return n.call(this,a,e);if(!a||0>r)return n.call(this,a,e-1);c=a[0],v.call(s,c)>=0?r+=1:(h=a[0],v.call(i,h)>=0&&(r-=1)),e+=1}return e-1},e.prototype.removeLeadingNewlines=function(){var e,t,n,i,s;for(s=this.tokens,e=n=0,i=s.length;i>n&&(t=s[e][0],"TERMINATOR"===t);e=++n);return e?this.tokens.splice(0,e):void 0},e.prototype.removeMidExpressionNewlines=function(){return this.scanTokens(function(e,t,i){var s;return"TERMINATOR"===e[0]&&(s=this.tag(t+1),v.call(n,s)>=0)?(i.splice(t,1),0):1})},e.prototype.closeOpenCalls=function(){var e,t;return t=function(e,t){var n;return")"===(n=e[0])||"CALL_END"===n||"OUTDENT"===e[0]&&")"===this.tag(t-1)},e=function(e,t){return this.tokens["OUTDENT"===e[0]?t-1:t][0]="CALL_END"},this.scanTokens(function(n,i){return"CALL_START"===n[0]&&this.detectEnd(i+1,t,e),1})},e.prototype.closeOpenIndexes=function(){var e,t;return t=function(e){var t;return"]"===(t=e[0])||"INDEX_END"===t},e=function(e){return e[0]="INDEX_END"},this.scanTokens(function(n,i){return"INDEX_START"===n[0]&&this.detectEnd(i+1,t,e),1})},e.prototype.matchTags=function(){var e,t,n,i,s,r,a;for(t=arguments[0],i=arguments.length>=2?w.call(arguments,1):[],e=0,n=s=0,r=i.length;r>=0?r>s:s>r;n=r>=0?++s:--s){for(;"HERECOMMENT"===this.tag(t+n+e);)e+=2;if(null!=i[n]&&("string"==typeof i[n]&&(i[n]=[i[n]]),a=this.tag(t+n+e),0>v.call(i[n],a)))return!1}return!0},e.prototype.looksObjectish=function(e){return this.matchTags(e,"@",null,":")||this.matchTags(e,null,":")},e.prototype.findTagsBackwards=function(e,t){var n,r,a,o,c,h,l;for(n=[];e>=0&&(n.length||(o=this.tag(e),0>v.call(t,o)&&(c=this.tag(e),0>v.call(s,c)||this.tokens[e].generated)&&(h=this.tag(e),0>v.call(u,h))));)r=this.tag(e),v.call(i,r)>=0&&n.push(this.tag(e)),a=this.tag(e),v.call(s,a)>=0&&n.length&&n.pop(),e-=1;return l=this.tag(e),v.call(t,l)>=0},e.prototype.addImplicitBracesAndParens=function(){var e;return e=[],this.scanTokens(function(t,n,r){var l,p,d,m,g,b,k,y,w,T,C,F,L,E,N,x,D,S,A,R,I,_,$,O,M,j;if(R=t[0],T=(n>0?r[n-1]:[])[0],y=(r.length-1>n?r[n+1]:[])[0],N=function(){return e[e.length-1]},x=n,d=function(e){return n-x+e},m=function(){var e,t;return null!=(e=N())?null!=(t=e[2])?t.ours:void 0:void 0},g=function(){var e;return m()&&"("===(null!=(e=N())?e[0]:void 0)},k=function(){var e;return m()&&"{"===(null!=(e=N())?e[0]:void 0)},b=function(){var e;return m&&"CONTROL"===(null!=(e=N())?e[0]:void 0)},D=function(t){var i;return i=null!=t?t:n,e.push(["(",i,{ours:!0}]),r.splice(i,0,f("CALL_START","(")),null==t?n+=1:void 0},l=function(){return e.pop(),r.splice(n,0,f("CALL_END",")")),n+=1},S=function(t,i){var s;return null==i&&(i=!0),s=null!=t?t:n,e.push(["{",s,{sameLine:!0,startsLine:i,ours:!0}]),r.splice(s,0,f("{",f(new String("{")))),null==t?n+=1:void 0},p=function(t){return t=null!=t?t:n,e.pop(),r.splice(t,0,f("}","}")),n+=1},g()&&("IF"===R||"TRY"===R||"FINALLY"===R||"CATCH"===R||"CLASS"===R||"SWITCH"===R))return e.push(["CONTROL",n,{ours:!0}]),d(1);if("INDENT"===R&&m()){if("=>"!==T&&"->"!==T&&"["!==T&&"("!==T&&","!==T&&"{"!==T&&"TRY"!==T&&"ELSE"!==T&&"="!==T)for(;g();)l();return b()&&e.pop(),e.push([R,n]),d(1)}if(v.call(s,R)>=0)return e.push([R,n]),d(1);if(v.call(i,R)>=0){for(;m();)g()?l():k()?p():e.pop();e.pop()}if((v.call(c,R)>=0&&t.spaced||"?"===R&&n>0&&!r[n-1].spaced)&&(v.call(a,y)>=0||v.call(h,y)>=0&&!(null!=(I=r[n+1])?I.spaced:void 0)&&!(null!=(_=r[n+1])?_.newLine:void 0)))return"?"===R&&(R=t[0]="FUNC_EXIST"),D(n+1),d(2);if(this.matchTags(n,c,"INDENT",null,":")&&!this.findTagsBackwards(n,["CLASS","EXTENDS","IF","CATCH","SWITCH","LEADING_WHEN","FOR","WHILE","UNTIL"]))return D(n+1),e.push(["INDENT",n+2]),d(3);if(":"===R){for(C="@"===this.tag(n-2)?n-2:n-1;"HERECOMMENT"===this.tag(C-2);)C-=2;return A=0===C||($=this.tag(C-1),v.call(u,$)>=0)||r[C-1].newLine,N()&&(O=N(),E=O[0],L=O[1],("{"===E||"INDENT"===E&&"{"===this.tag(L-1))&&(A||","===this.tag(C-1)||"{"===this.tag(C-1)))?d(1):(S(C,!!A),d(2))}if("OUTDENT"===T&&g()&&("."===R||"?."===R||"::"===R||"?::"===R))return l(),d(1);if(k()&&v.call(u,R)>=0&&(N()[2].sameLine=!1),v.call(o,R)>=0)for(;m();)if(M=N(),E=M[0],L=M[1],j=M[2],F=j.sameLine,A=j.startsLine,g()&&","!==T)l();else if(k()&&F&&!A)p();else{if(!k()||"TERMINATOR"!==R||","===T||A&&this.looksObjectish(n+1))break;p()}if(","===R&&!this.looksObjectish(n+1)&&k()&&("TERMINATOR"!==y||!this.looksObjectish(n+2)))for(w="OUTDENT"===y?1:0;k();)p(n+w);return d(1)})},e.prototype.addLocationDataToGeneratedTokens=function(){return this.scanTokens(function(e,t,n){var i,s,r,a,o;return e[2]?1:e.generated||e.explicit?(o=null!=(r=null!=(a=n[t-1])?a[2]:void 0)?r:{last_line:0,last_column:0},s=o.last_line,i=o.last_column,e[2]={first_line:s,first_column:i,last_line:s,last_column:i},1):1})},e.prototype.addImplicitIndentation=function(){var e,t,n,i,s;return s=n=i=null,t=function(e){var t;return";"!==e[1]&&(t=e[0],v.call(p,t)>=0)&&!("ELSE"===e[0]&&"IF"!==s&&"THEN"!==s)},e=function(e,t){return this.tokens.splice(","===this.tag(t-1)?t-1:t,0,i)},this.scanTokens(function(r,a,o){var c,h,l;return c=r[0],"TERMINATOR"===c&&"THEN"===this.tag(a+1)?(o.splice(a,1),0):"ELSE"===c&&"OUTDENT"!==this.tag(a-1)?(o.splice.apply(o,[a,0].concat(w.call(this.indentation(r)))),2):"CATCH"!==c||"OUTDENT"!==(h=this.tag(a+2))&&"TERMINATOR"!==h&&"FINALLY"!==h?v.call(d,c)>=0&&"INDENT"!==this.tag(a+1)&&("ELSE"!==c||"IF"!==this.tag(a+1))?(s=c,l=this.indentation(r,!0),n=l[0],i=l[1],"THEN"===s&&(n.fromThen=!0),o.splice(a+1,0,n),this.detectEnd(a+2,t,e),"THEN"===c&&o.splice(a,1),1):1:(o.splice.apply(o,[a+2,0].concat(w.call(this.indentation(r)))),4)})},e.prototype.tagPostfixConditionals=function(){var e,t,n;return n=null,t=function(e){var t;return"TERMINATOR"===(t=e[0])||"INDENT"===t},e=function(e){return"INDENT"!==e[0]||e.generated&&!e.fromThen?n[0]="POST_"+n[0]:void 0},this.scanTokens(function(i,s){return"IF"!==i[0]?1:(n=i,this.detectEnd(s+1,t,e),1)})},e.prototype.indentation=function(e,t){var n,i;return null==t&&(t=!1),n=["INDENT",2],i=["OUTDENT",2],t&&(n.generated=i.generated=!0),t||(n.explicit=i.explicit=!0),[n,i]},e.prototype.generate=f,e.prototype.tag=function(e){var t;return null!=(t=this.tokens[e])?t[0]:void 0},e}(),t=[["(",")"],["[","]"],["{","}"],["INDENT","OUTDENT"],["CALL_START","CALL_END"],["PARAM_START","PARAM_END"],["INDEX_START","INDEX_END"]],e.INVERSES=l={},s=[],i=[],b=0,k=t.length;k>b;b++)y=t[b],m=y[0],g=y[1],s.push(l[g]=m),i.push(l[m]=g);n=["CATCH","WHEN","ELSE","FINALLY"].concat(i),c=["IDENTIFIER","SUPER",")","CALL_END","]","INDEX_END","@","THIS"],a=["IDENTIFIER","NUMBER","STRING","JS","REGEX","NEW","PARAM_START","CLASS","IF","TRY","SWITCH","THIS","BOOL","NULL","UNDEFINED","UNARY","SUPER","@","->","=>","[","(","{","--","++"],h=["+","-"],r=["->","=>","{","[",","],o=["POST_IF","FOR","WHILE","UNTIL","WHEN","BY","LOOP","TERMINATOR"],d=["ELSE","->","=>","TRY","FINALLY","THEN"],p=["TERMINATOR","CATCH","FINALLY","ELSE","OUTDENT","LEADING_WHEN"],u=["TERMINATOR","INDENT","OUTDENT"]}).call(this)},require["./lexer"]=new function(){var e=this;(function(){var t,n,i,s,r,a,o,c,h,l,u,p,d,f,m,g,b,k,y,v,w,T,C,F,L,E,N,x,D,S,A,R,I,_,$,O,M,j,B,V,P,U,q,H,G,W,X,Y,K,z,J,Z=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1};z=require("./rewriter"),M=z.Rewriter,k=z.INVERSES,J=require("./helpers"),G=J.count,K=J.starts,H=J.compact,X=J.last,Y=J.locationDataToString,e.Lexer=E=function(){function e(){}return e.prototype.tokenize=function(e,t){var n,i,s,r;for(null==t&&(t={}),this.literate=t.literate,this.indent=0,this.indebt=0,this.outdebt=0,this.indents=[],this.ends=[],this.tokens=[],this.chunkLine=t.line||0,this.chunkColumn=t.column||0,e=this.clean(e),i=0;this.chunk=e.slice(i);)n=this.identifierToken()||this.commentToken()||this.whitespaceToken()||this.lineToken()||this.heredocToken()||this.stringToken()||this.numberToken()||this.regexToken()||this.jsToken()||this.literalToken(),r=this.getLineAndColumnFromChunk(n),this.chunkLine=r[0],this.chunkColumn=r[1],i+=n;return this.closeIndentation(),(s=this.ends.pop())&&this.error("missing "+s),t.rewrite===!1?this.tokens:(new M).rewrite(this.tokens)},e.prototype.clean=function(e){var n,i,s;return e.charCodeAt(0)===t&&(e=e.slice(1)),e=e.replace(/\r/g,"").replace(P,""),q.test(e)&&(e="\n"+e,this.chunkLine--),this.literate&&(i=function(){var t,i,r,a;for(r=e.split("\n"),a=[],t=0,i=r.length;i>t;t++)n=r[t],(s=F.exec(n))?a.push(n.slice(s[0].length)):a.push("# "+n);return a}(),e=i.join("\n")),e},e.prototype.identifierToken=function(){var e,t,n,i,s,c,h,l,u,p,d,f,m,b;return(h=g.exec(this.chunk))?(c=h[0],i=h[1],e=h[2],s=i.length,l=void 0,"own"===i&&"FOR"===this.tag()?(this.token("OWN",i),i.length):(n=e||(u=X(this.tokens))&&("."===(f=u[0])||"?."===f||"::"===f||"?::"===f||!u.spaced&&"@"===u[0]),p="IDENTIFIER",!n&&(Z.call(w,i)>=0||Z.call(o,i)>=0)&&(p=i.toUpperCase(),"WHEN"===p&&(m=this.tag(),Z.call(T,m)>=0)?p="LEADING_WHEN":"FOR"===p?this.seenFor=!0:"UNLESS"===p?p="IF":Z.call(U,p)>=0?p="UNARY":Z.call($,p)>=0&&("INSTANCEOF"!==p&&this.seenFor?(p="FOR"+p,this.seenFor=!1):(p="RELATION","!"===this.value()&&(l=this.tokens.pop(),i="!"+i)))),Z.call(v,i)>=0&&(n?(p="IDENTIFIER",i=new String(i),i.reserved=!0):Z.call(O,i)>=0&&this.error('reserved word "'+i+'"')),n||(Z.call(r,i)>=0&&(i=a[i]),p=function(){switch(i){case"!":return"UNARY";case"==":case"!=":return"COMPARE";case"&&":case"||":return"LOGIC";case"true":case"false":return"BOOL";case"break":case"continue":return"STATEMENT";default:return p}}()),d=this.token(p,i,0,s),l&&(b=[l[2].first_line,l[2].first_column],d[2].first_line=b[0],d[2].first_column=b[1]),e&&(t=c.lastIndexOf(":"),this.token(":",":",t,e.length)),c.length)):0},e.prototype.numberToken=function(){var e,t,n,i,s;return(n=R.exec(this.chunk))?(i=n[0],/^0[BOX]/.test(i)?this.error("radix prefix '"+i+"' must be lowercase"):/E/.test(i)&&!/^0x/.test(i)?this.error("exponential notation '"+i+"' must be indicated with a lowercase 'e'"):/^0\d*[89]/.test(i)?this.error("decimal literal '"+i+"' must not be prefixed with '0'"):/^0\d+/.test(i)&&this.error("octal literal '"+i+"' must be prefixed with '0o'"),t=i.length,(s=/^0o([0-7]+)/.exec(i))&&(i="0x"+parseInt(s[1],8).toString(16)),(e=/^0b([01]+)/.exec(i))&&(i="0x"+parseInt(e[1],2).toString(16)),this.token("NUMBER",i,0,t),t):0},e.prototype.stringToken=function(){var e,t,n;switch(this.chunk.charAt(0)){case"'":if(!(e=B.exec(this.chunk)))return 0;n=e[0],this.token("STRING",n.replace(x,"\\\n"),0,n.length);break;case'"':if(!(n=this.balancedString(this.chunk,'"')))return 0;n.indexOf("#{",1)>0?this.interpolateString(n.slice(1,-1),{strOffset:1,lexedLength:n.length}):this.token("STRING",this.escapeLines(n,0,n.length));break;default:return 0}return(t=/^(?:\\.|[^\\])*\\(?:0[0-7]|[1-7])/.test(n))&&this.error("octal escape sequences "+n+" are not allowed"),n.length},e.prototype.heredocToken=function(){var e,t,n,i;return(n=u.exec(this.chunk))?(t=n[0],i=t.charAt(0),e=this.sanitizeHeredoc(n[2],{quote:i,indent:null}),'"'===i&&e.indexOf("#{")>=0?this.interpolateString(e,{heredoc:!0,strOffset:3,lexedLength:t.length}):this.token("STRING",this.makeString(e,i,!0),0,t.length),t.length):0},e.prototype.commentToken=function(){var e,t,n;return(n=this.chunk.match(c))?(e=n[0],t=n[1],t&&this.token("HERECOMMENT",this.sanitizeHeredoc(t,{herecomment:!0,indent:Array(this.indent+1).join(" ")}),0,e.length),e.length):0},e.prototype.jsToken=function(){var e,t;return"`"===this.chunk.charAt(0)&&(e=y.exec(this.chunk))?(this.token("JS",(t=e[0]).slice(1,-1),0,t.length),t.length):0},e.prototype.regexToken=function(){var e,t,n,i,s,r,a;return"/"!==this.chunk.charAt(0)?0:(n=f.exec(this.chunk))?t=this.heregexToken(n):(i=X(this.tokens),i&&(r=i[0],Z.call(i.spaced?S:A,r)>=0)?0:(n=_.exec(this.chunk))?(a=n,n=a[0],s=a[1],e=a[2],"/*"===s.slice(0,2)&&this.error("regular expressions cannot begin with `*`"),"//"===s&&(s="/(?:)/"),this.token("REGEX",""+s+e,0,n.length),n.length):0)},e.prototype.heregexToken=function(e){var t,n,i,s,r,a,o,c,h,l,u,p,d,f,g,b;if(s=e[0],t=e[1],n=e[2],0>t.indexOf("#{"))return o=t.replace(m,"").replace(/\//g,"\\/"),o.match(/^\*/)&&this.error("regular expressions cannot begin with `*`"),this.token("REGEX","/"+(o||"(?:)")+"/"+n,0,s.length),s.length;for(this.token("IDENTIFIER","RegExp",0,0),this.token("CALL_START","(",0,0),l=[],f=this.interpolateString(t,{regex:!0}),p=0,d=f.length;d>p;p++){if(h=f[p],c=h[0],u=h[1],"TOKENS"===c)l.push.apply(l,u);else if("NEOSTRING"===c){if(!(u=u.replace(m,"")))continue;u=u.replace(/\\/g,"\\\\"),h[0]="STRING",h[1]=this.makeString(u,'"',!0),l.push(h)}else this.error("Unexpected "+c);a=X(this.tokens),r=["+","+"],r[2]=a[2],l.push(r)}return l.pop(),"STRING"!==(null!=(g=l[0])?g[0]:void 0)&&(this.token("STRING",'""',0,0),this.token("+","+",0,0)),(b=this.tokens).push.apply(b,l),n&&(i=s.lastIndexOf(n),this.token(",",",",i,0),this.token("STRING",'"'+n+'"',i,n.length)),this.token(")",")",s.length-1,0),s.length},e.prototype.lineToken=function(){var e,t,n,i,s;if(!(n=D.exec(this.chunk)))return 0;if(t=n[0],this.seenFor=!1,s=t.length-1-t.lastIndexOf("\n"),i=this.unfinished(),s-this.indebt===this.indent)return i?this.suppressNewlines():this.newlineToken(0),t.length;if(s>this.indent){if(i)return this.indebt=s-this.indent,this.suppressNewlines(),t.length;e=s-this.indent+this.outdebt,this.token("INDENT",e,0,t.length),this.indents.push(e),this.ends.push("OUTDENT"),this.outdebt=this.indebt=0}else this.indebt=0,this.outdentToken(this.indent-s,i,t.length);return this.indent=s,t.length},e.prototype.outdentToken=function(e,t,n){for(var i,s;e>0;)s=this.indents.length-1,void 0===this.indents[s]?e=0:this.indents[s]===this.outdebt?(e-=this.outdebt,this.outdebt=0):this.indents[s]
Latest Version: - 1.6.1 + 1.6.3
@@ -359,16 +357,6 @@directly to stdout.
-l, --lint
-s, --stdio
-e, --eval
-l, --literate
-r, --require
-e, --eval
-$('.account').attr({ +
$('.account').attr({ "class": 'active' }); @@ -1272,19 +1260,23 @@"And the error is ... #{error}" ) -
+
var error; + alert((function() { try { return nonexistent / void 0; - } catch (error) { + } catch (_error) { + error = _error; return "And the error is ... " + error; } })()); -@@ -1786,6 +1774,35 @@
+ Destructuring assignment is also useful when combined with class constructors + to assign properties to your instance from an options object passed to the constructor. +
+class Person + constructor: (options) -> + {@name, @age, @height} = options + +
var Person; + +Person = (function() { + function Person(options) { + this.name = options.name, this.age = options.age, this.height = options.height; + } + + return Person; + +})(); +
@@ -1822,7 +1839,7 @@
If we had used -> in the callback above, @customer would have referred to the undefined "customer" property of the DOM element, @@ -1852,7 +1869,7 @@
+ Switch statements can also be used without a control expression, turning them in to a cleaner alternative to if/else chains. +
+score = 76 +grade = switch + when score < 60 then 'F' + when score < 70 then 'D' + when score < 80 then 'C' + when score < 90 then 'B' + else 'A' +# grade == 'C' +
var grade, score; + +score = 76; + +grade = (function() { + switch (false) { + case !(score < 60): + return 'F'; + case !(score < 70): + return 'D'; + case !(score < 80): + return 'C'; + case !(score < 90): + return 'B'; + default: + return 'A'; + } +})(); +
@@ -1924,16 +1971,18 @@
+
var error; + try { allHellBreaksLoose(); catsAndDogsLivingTogether(); -} catch (error) { +} catch (_error) { + error = _error; print(error); } finally { cleanUp(); } -
@@ -1953,7 +2002,7 @@
var html; html = "<strong>\n cup of coffeescript\n</strong>"; -
### -CoffeeScript Compiler v1.6.1 +SkinnyMochaHalfCaffScript Compiler v1.0 Released under the MIT License ###
/* -CoffeeScript Compiler v1.6.1 +SkinnyMochaHalfCaffScript Compiler v1.0 Released under the MIT License */ -
Block Regular Expressions Similar to block strings and comments, CoffeeScript supports block regexes — extended regular expressions that ignore internal whitespace and can contain - comments and interpolation. Modeled after Perl's /x modifier, CoffeeSctipt's + comments and interpolation. Modeled after Perl's /x modifier, CoffeeScript's block regexes are delimited by /// and go a long way towards making complex regular expressions readable. To quote from the CoffeeScript source:
@@ -2074,7 +2123,7 @@var OPERATOR; OPERATOR = /^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>])\2=?|\?\.|\.{2,3})/; -
If you need to invoke one task before another — for example, running build before test, you can use the invoke function: @@ -2335,6 +2384,13 @@
+ + 1.6.3 + + +
+ + 1.6.2 + + +
1.6.1
@@ -3109,8 +3223,10 @@
else
$(el).text window.compiledJS
$('#error').hide()
- catch error
- $('#error').text(error.message).show()
+ catch {location, message}
+ if location?
+ message = "Error on line #{location.first_line + 1}: #{message}"
+ $('#error').text(message).show()
# Update permalink
$('#repl_permalink').attr 'href', "##{sourceFragment}#{encodeURIComponent source}"
diff --git a/lib/coffee-script/browser.js b/lib/coffee-script/browser.js
index fec8e6901c..e5411d8c2b 100644
--- a/lib/coffee-script/browser.js
+++ b/lib/coffee-script/browser.js
@@ -1,21 +1,22 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
(function() {
- var CoffeeScript, runScripts,
+ var CoffeeScript, compile, runScripts,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
CoffeeScript = require('./coffee-script');
CoffeeScript.require = require;
+ compile = CoffeeScript.compile;
+
CoffeeScript["eval"] = function(code, options) {
- var _ref;
if (options == null) {
options = {};
}
- if ((_ref = options.bare) == null) {
+ if (options.bare == null) {
options.bare = true;
}
- return eval(CoffeeScript.compile(code, options));
+ return eval(compile(code, options));
};
CoffeeScript.run = function(code, options) {
@@ -23,19 +24,34 @@
options = {};
}
options.bare = true;
- return Function(CoffeeScript.compile(code, options))();
+ options.shiftLine = true;
+ return Function(compile(code, options))();
};
if (typeof window === "undefined" || window === null) {
return;
}
+ if ((typeof btoa !== "undefined" && btoa !== null) && (typeof JSON !== "undefined" && JSON !== null) && (typeof unescape !== "undefined" && unescape !== null) && (typeof encodeURIComponent !== "undefined" && encodeURIComponent !== null)) {
+ compile = function(code, options) {
+ var js, v3SourceMap, _ref;
+ if (options == null) {
+ options = {};
+ }
+ options.sourceMap = true;
+ options.inline = true;
+ _ref = CoffeeScript.compile(code, options), js = _ref.js, v3SourceMap = _ref.v3SourceMap;
+ return "" + js + "\n//@ sourceMappingURL=data:application/json;base64," + (btoa(unescape(encodeURIComponent(v3SourceMap)))) + "\n//@ sourceURL=coffeescript";
+ };
+ }
+
CoffeeScript.load = function(url, callback, options) {
var xhr;
if (options == null) {
options = {};
}
- xhr = window.ActiveXObject ? new window.ActiveXObject('Microsoft.XMLHTTP') : new XMLHttpRequest();
+ options.sourceFiles = [url];
+ xhr = window.ActiveXObject ? new window.ActiveXObject('Microsoft.XMLHTTP') : new window.XMLHttpRequest();
xhr.open('GET', url, true);
if ('overrideMimeType' in xhr) {
xhr.overrideMimeType('text/plain');
@@ -58,7 +74,7 @@
runScripts = function() {
var coffees, coffeetypes, execute, index, length, s, scripts;
- scripts = document.getElementsByTagName('script');
+ scripts = window.document.getElementsByTagName('script');
coffeetypes = ['text/coffeescript', 'text/literate-coffeescript'];
coffees = (function() {
var _i, _len, _ref, _results;
@@ -84,6 +100,7 @@
if (script.src) {
return CoffeeScript.load(script.src, execute, options);
} else {
+ options.sourceFiles = ['embedded'];
CoffeeScript.run(script.innerHTML, options);
return execute();
}
@@ -93,9 +110,9 @@
};
if (window.addEventListener) {
- addEventListener('DOMContentLoaded', runScripts, false);
+ window.addEventListener('DOMContentLoaded', runScripts, false);
} else {
- attachEvent('onload', runScripts);
+ window.attachEvent('onload', runScripts);
}
}).call(this);
diff --git a/lib/coffee-script/cake.js b/lib/coffee-script/cake.js
index a0b643f665..68bd7c30f8 100644
--- a/lib/coffee-script/cake.js
+++ b/lib/coffee-script/cake.js
@@ -1,4 +1,4 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
(function() {
var CoffeeScript, cakefileDirectory, existsSync, fatalError, fs, helpers, missingTask, oparse, options, optparse, path, printTasks, switches, tasks;
@@ -46,7 +46,7 @@
});
exports.run = function() {
- var arg, args, _i, _len, _ref, _results;
+ var arg, args, e, _i, _len, _ref, _results;
global.__originalDirname = fs.realpathSync('.');
process.chdir(cakefileDirectory(__originalDirname));
args = process.argv.slice(2);
@@ -59,7 +59,8 @@
}
try {
options = oparse.parse(args);
- } catch (e) {
+ } catch (_error) {
+ e = _error;
return fatalError("" + e);
}
_ref = options["arguments"];
diff --git a/lib/coffee-script/coffee-script.js b/lib/coffee-script/coffee-script.js
index d791035d30..11ccd8104b 100644
--- a/lib/coffee-script/coffee-script.js
+++ b/lib/coffee-script/coffee-script.js
@@ -1,85 +1,60 @@
-// Generated by CoffeeScript 1.6.1
+// Generated by CoffeeScript 1.6.3
(function() {
- var Lexer, compile, ext, fs, helpers, lexer, loadFile, parser, path, sourcemap, vm, _i, _len, _ref,
+ var Lexer, Module, SourceMap, child_process, compile, ext, findExtension, fork, formatSourcePosition, fs, helpers, lexer, loadFile, parser, patchStackTrace, patched, path, sourceMaps, vm, _i, _len, _ref,
__hasProp = {}.hasOwnProperty;
fs = require('fs');
+ vm = require('vm');
+
path = require('path');
+ child_process = require('child_process');
+
Lexer = require('./lexer').Lexer;
parser = require('./parser').parser;
helpers = require('./helpers');
- vm = require('vm');
-
- sourcemap = require('./sourcemap');
-
- loadFile = function(module, filename) {
- var raw, stripped;
- raw = fs.readFileSync(filename, 'utf8');
- stripped = raw.charCodeAt(0) === 0xFEFF ? raw.substring(1) : raw;
- return module._compile(compile(stripped, {
- filename: filename,
- literate: helpers.isLiterate(filename)
- }), filename);
- };
-
- if (require.extensions) {
- _ref = ['.coffee', '.litcoffee', '.md', '.coffee.md'];
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- ext = _ref[_i];
- require.extensions[ext] = loadFile;
- }
- }
+ SourceMap = require('./sourcemap');
- exports.VERSION = '1.6.1';
+ exports.VERSION = '1.6.3';
exports.helpers = helpers;
exports.compile = compile = function(code, options) {
- var answer, coffeeFile, currentColumn, currentLine, fragment, fragments, header, js, jsFile, merge, newLines, sourceMap, _j, _len1;
+ var answer, currentColumn, currentLine, fragment, fragments, header, js, map, merge, newLines, _i, _len;
if (options == null) {
options = {};
}
- merge = exports.helpers.merge;
- try {
- if (options.sourceMap) {
- coffeeFile = helpers.baseFileName(options.filename);
- jsFile = helpers.baseFileName(options.filename, true) + ".js";
- sourceMap = new sourcemap.SourceMap();
- }
- fragments = (parser.parse(lexer.tokenize(code, options))).compileToFragments(options);
- currentLine = 0;
- if (options.header) {
- currentLine += 1;
- }
+ merge = helpers.merge;
+ if (options.sourceMap) {
+ map = new SourceMap;
+ }
+ fragments = parser.parse(lexer.tokenize(code, options)).compileToFragments(options);
+ currentLine = 0;
+ if (options.header) {
+ currentLine += 1;
+ }
+ if (options.shiftLine) {
+ currentLine += 1;
+ }
+ currentColumn = 0;
+ js = "";
+ for (_i = 0, _len = fragments.length; _i < _len; _i++) {
+ fragment = fragments[_i];
if (options.sourceMap) {
- currentLine += 1;
- }
- currentColumn = 0;
- js = "";
- for (_j = 0, _len1 = fragments.length; _j < _len1; _j++) {
- fragment = fragments[_j];
- if (sourceMap) {
- if (fragment.locationData) {
- sourceMap.addMapping([fragment.locationData.first_line, fragment.locationData.first_column], [currentLine, currentColumn], {
- noReplace: true
- });
- }
- newLines = helpers.count(fragment.code, "\n");
- currentLine += newLines;
- currentColumn = fragment.code.length - (newLines ? fragment.code.lastIndexOf("\n") : 0);
+ if (fragment.locationData) {
+ map.add([fragment.locationData.first_line, fragment.locationData.first_column], [currentLine, currentColumn], {
+ noReplace: true
+ });
}
- js += fragment.code;
- }
- } catch (err) {
- if (options.filename) {
- err.message = "In " + options.filename + ", " + err.message;
+ newLines = helpers.count(fragment.code, "\n");
+ currentLine += newLines;
+ currentColumn = fragment.code.length - (newLines ? fragment.code.lastIndexOf("\n") : 0);
}
- throw err;
+ js += fragment.code;
}
if (options.header) {
header = "Generated by CoffeeScript " + this.VERSION;
@@ -89,10 +64,8 @@
answer = {
js: js
};
- if (sourceMap) {
- answer.sourceMap = sourceMap;
- answer.v3SourceMap = sourcemap.generateV3SourceMap(sourceMap, coffeeFile, jsFile);
- }
+ answer.sourceMap = map;
+ answer.v3SourceMap = map.generate(options, code);
return answer;
} else {
return js;
@@ -112,23 +85,29 @@
};
exports.run = function(code, options) {
- var mainModule;
+ var answer, mainModule;
if (options == null) {
options = {};
}
mainModule = require.main;
+ if (options.sourceMap == null) {
+ options.sourceMap = true;
+ }
mainModule.filename = process.argv[1] = options.filename ? fs.realpathSync(options.filename) : '.';
mainModule.moduleCache && (mainModule.moduleCache = {});
- mainModule.paths = require('module')._nodeModulePaths(path.dirname(fs.realpathSync(options.filename)));
+ mainModule.paths = require('module')._nodeModulePaths(path.dirname(fs.realpathSync(options.filename || '.')));
if (!helpers.isCoffee(mainModule.filename) || require.extensions) {
- return mainModule._compile(compile(code, options), mainModule.filename);
+ answer = compile(code, options);
+ patchStackTrace();
+ sourceMaps[mainModule.filename] = answer.sourceMap;
+ return mainModule._compile(answer.js, mainModule.filename);
} else {
return mainModule._compile(code, mainModule.filename);
}
};
exports["eval"] = function(code, options) {
- var Module, Script, js, k, o, r, sandbox, v, _j, _len1, _module, _ref1, _ref2, _require;
+ var Module, Script, js, k, o, r, sandbox, v, _i, _len, _module, _ref, _ref1, _require;
if (options == null) {
options = {};
}
@@ -142,10 +121,10 @@
sandbox = options.sandbox;
} else {
sandbox = Script.createContext();
- _ref1 = options.sandbox;
- for (k in _ref1) {
- if (!__hasProp.call(_ref1, k)) continue;
- v = _ref1[k];
+ _ref = options.sandbox;
+ for (k in _ref) {
+ if (!__hasProp.call(_ref, k)) continue;
+ v = _ref[k];
sandbox[k] = v;
}
}
@@ -162,9 +141,9 @@
return Module._load(path, _module, true);
};
_module.filename = sandbox.__filename;
- _ref2 = Object.getOwnPropertyNames(require);
- for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
- r = _ref2[_j];
+ _ref1 = Object.getOwnPropertyNames(require);
+ for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
+ r = _ref1[_i];
if (r !== 'paths') {
_require[r] = require[r];
}
@@ -190,6 +169,70 @@
}
};
+ loadFile = function(module, filename) {
+ var answer, raw, stripped;
+ raw = fs.readFileSync(filename, 'utf8');
+ stripped = raw.charCodeAt(0) === 0xFEFF ? raw.substring(1) : raw;
+ answer = compile(stripped, {
+ filename: filename,
+ sourceMap: true,
+ literate: helpers.isLiterate(filename)
+ });
+ sourceMaps[filename] = answer.sourceMap;
+ return module._compile(answer.js, filename);
+ };
+
+ if (require.extensions) {
+ _ref = ['.coffee', '.litcoffee', '.coffee.md'];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ ext = _ref[_i];
+ require.extensions[ext] = loadFile;
+ }
+ Module = require('module');
+ findExtension = function(filename) {
+ var curExtension, extensions;
+ extensions = path.basename(filename).split('.');
+ if (extensions[0] === '') {
+ extensions.shift();
+ }
+ while (extensions.shift()) {
+ curExtension = '.' + extensions.join('.');
+ if (Module._extensions[curExtension]) {
+ return curExtension;
+ }
+ }
+ return '.js';
+ };
+ Module.prototype.load = function(filename) {
+ var extension;
+ this.filename = filename;
+ this.paths = Module._nodeModulePaths(path.dirname(filename));
+ extension = findExtension(filename);
+ Module._extensions[extension](this, filename);
+ return this.loaded = true;
+ };
+ }
+
+ if (child_process) {
+ fork = child_process.fork;
+ child_process.fork = function(path, args, options) {
+ var execPath;
+ if (args == null) {
+ args = [];
+ }
+ if (options == null) {
+ options = {};
+ }
+ execPath = helpers.isCoffee(path) ? 'coffee' : null;
+ if (!Array.isArray(args)) {
+ args = [];
+ options = args || {};
+ }
+ options.execPath || (options.execPath = execPath);
+ return fork(path, args, options);
+ };
+ }
+
lexer = new Lexer;
parser.lexer = {
@@ -215,4 +258,101 @@
parser.yy = require('./nodes');
+ parser.yy.parseError = function(message, _arg) {
+ var token;
+ token = _arg.token;
+ message = "unexpected " + (token === 1 ? 'end of input' : token);
+ return helpers.throwSyntaxError(message, parser.lexer.yylloc);
+ };
+
+ patched = false;
+
+ sourceMaps = {};
+
+ patchStackTrace = function() {
+ var mainModule;
+ if (patched) {
+ return;
+ }
+ patched = true;
+ mainModule = require.main;
+ return Error.prepareStackTrace = function(err, stack) {
+ var frame, frames, getSourceMapping, sourceFiles, _ref1;
+ sourceFiles = {};
+ getSourceMapping = function(filename, line, column) {
+ var answer, sourceMap;
+ sourceMap = sourceMaps[filename];
+ if (sourceMap) {
+ answer = sourceMap.sourceLocation([line - 1, column - 1]);
+ }
+ if (answer) {
+ return [answer[0] + 1, answer[1] + 1];
+ } else {
+ return null;
+ }
+ };
+ frames = (function() {
+ var _j, _len1, _results;
+ _results = [];
+ for (_j = 0, _len1 = stack.length; _j < _len1; _j++) {
+ frame = stack[_j];
+ if (frame.getFunction() === exports.run) {
+ break;
+ }
+ _results.push(" at " + (formatSourcePosition(frame, getSourceMapping)));
+ }
+ return _results;
+ })();
+ return "" + err.name + ": " + ((_ref1 = err.message) != null ? _ref1 : '') + "\n" + (frames.join('\n')) + "\n";
+ };
+ };
+
+ formatSourcePosition = function(frame, getSourceMapping) {
+ var as, column, fileLocation, fileName, functionName, isConstructor, isMethodCall, line, methodName, source, tp, typeName;
+ fileName = void 0;
+ fileLocation = '';
+ if (frame.isNative()) {
+ fileLocation = "native";
+ } else {
+ if (frame.isEval()) {
+ fileName = frame.getScriptNameOrSourceURL();
+ if (!fileName) {
+ fileLocation = "" + (frame.getEvalOrigin()) + ", ";
+ }
+ } else {
+ fileName = frame.getFileName();
+ }
+ fileName || (fileName = "
CoffeeScript Test Suite
'assignment'
'booleans'
'classes'
+ 'cluster'
'comments'
'compilation'
'comprehensions'
@@ -111,6 +112,8 @@ CoffeeScript Test Suite
'soaks'
'strings'
]
+ # allow utf-8 chars in comments
+ # 智に働けば角が立つ、情に掉させば流される。