Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hygienic macro proposal #3171

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cakefile
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ runTests = (CoffeeScript) ->
currentFile = filename = path.join 'test', file
code = fs.readFileSync filename
try
CoffeeScript.run code.toString(), {filename, literate}
CoffeeScript.run code.toString(), {filename, literate, macro: true}
catch error
failures.push {filename, error}
return !failures.length
Expand Down
4 changes: 2 additions & 2 deletions src/browser.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ if btoa? and JSON? and unescape? and encodeURIComponent?

# Load a remote script from the current domain via XHR.
CoffeeScript.load = (url, callback, options = {}) ->
options.sourceFiles = [url]
options.filename = url
xhr = if window.ActiveXObject
new window.ActiveXObject('Microsoft.XMLHTTP')
else
Expand Down Expand Up @@ -66,7 +66,7 @@ runScripts = ->
if script.src
CoffeeScript.load script.src, execute, options
else
options.sourceFiles = ['embedded']
options.filename = 'embedded'
CoffeeScript.run script.innerHTML, options
execute()
null
Expand Down
122 changes: 41 additions & 81 deletions src/coffee-script.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ child_process = require 'child_process'
{parser} = require './parser'
helpers = require './helpers'
SourceMap = require './sourcemap'
Macro = require './macro'

# The current CoffeeScript version number.
exports.VERSION = '1.6.3'
Expand All @@ -21,79 +22,49 @@ fileExtensions = ['.coffee', '.litcoffee', '.coffee.md']
# Expose helpers for testing.
exports.helpers = helpers

# Function wrapper to add source file information to SyntaxErrors thrown by the
# lexer/parser/compiler.
withPrettyErrors = (fn) ->
(code, options = {}) ->
try
fn.call @, code, options
catch err
throw helpers.updateSyntaxError err, code, options.filename

# Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler.
#
# If `options.sourceMap` is specified, then `options.filename` must also be specified. All
# options that can be passed to `SourceMap#generate` may also be passed here.
#
# This returns a javascript string, unless `options.sourceMap` is passed,
# in which case this returns a `{js, v3SourceMap, sourceMap}`
# object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic
# lookups.
exports.compile = compile = withPrettyErrors (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
if newLines
currentColumn = fragment.code.length - (fragment.code.lastIndexOf("\n") + 1)
else
currentColumn += fragment.code.length

# Copy the code from each fragment into the final JavaScript.
js += fragment.code
exports.compile = compile = (code, options = {}) ->
ast = exports.nodes code, options
ast = Macro.expand(ast, exports.nodes) if options.macro
fragments = ast.compileToFragments options

js = (fragment.code for fragment in fragments).join('')

if options.header
header = "Generated by CoffeeScript #{@VERSION}"
js = "// #{header}\n#{js}"
js = "// Generated by CoffeeScript #{@VERSION}\n" + js

if options.sourceMap
answer = {js}
answer.sourceMap = map
answer.v3SourceMap = map.generate(options, code)
answer
sourceMap = new SourceMap fragments, options
{js, sourceMap, v3SourceMap: sourceMap.generate(options)}
else
js

# Tokenize a string of CoffeeScript code, and return the array of tokens.
exports.tokens = withPrettyErrors (code, options) ->
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 `.compile()` on the root,
# or traverse it by using `.traverseChildren()` with a callback.
exports.nodes = withPrettyErrors (source, options) ->
exports.nodes = (source, options={}) ->
fileNum = helpers.getFileNum source, options.filename
if typeof source is 'string'
parser.parse lexer.tokenize source, options
else
parser.parse source
source = exports.tokens source, options

nodes = parser.parse source

# Set the `fileNum` on each of the ast nodes.
parser.yy.walk nodes, (n) ->
n.locationData.file_num = fileNum if n.locationData
return
nodes


# Compile and execute a string of CoffeeScript (on the server), correctly
# setting `__filename`, `__dirname`, and relative `require()`.
Expand All @@ -117,6 +88,8 @@ exports.run = (code, options = {}) ->

mainModule._compile code, mainModule.filename

exports.run.stopStackTrace = true

# 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 = {}) ->
Expand Down Expand Up @@ -156,16 +129,7 @@ exports.eval = (code, options = {}) ->
compileFile = (filename, sourceMap) ->
raw = fs.readFileSync filename, 'utf8'
stripped = if raw.charCodeAt(0) is 0xFEFF then raw.substring 1 else raw

try
answer = compile(stripped, {filename, sourceMap, literate: helpers.isLiterate filename})
catch err
# As the filename and code of a dynamically loaded file will be different
# from the original file compiled with CoffeeScript.run, add that
# information to error so it can be pretty-printed later.
throw helpers.updateSyntaxError err, stripped, filename

answer
compile(stripped, {filename, sourceMap, literate: helpers.isLiterate filename})

# Load and run a CoffeeScript file for Node, stripping any `BOM`s.
loadFile = (module, filename) ->
Expand Down Expand Up @@ -235,7 +199,7 @@ parser.lexer =
""
# 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.
Expand All @@ -248,7 +212,7 @@ parser.yy.parseError = (message, {token}) ->

# Based on http://v8.googlecode.com/svn/branches/bleeding_edge/src/messages.js
# Modified to handle sourceMap
formatSourcePosition = (frame, getSourceMapping) ->
formatSourcePosition = (frame, defSrcMap) ->
fileName = undefined
fileLocation = ''

Expand All @@ -257,22 +221,23 @@ formatSourcePosition = (frame, getSourceMapping) ->
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]}"
else
"#{fileName}:#{line}:#{column}"
if fileName
# Check for a sourceMap position
sourceMap = getSourceMap fileName
else
sourceMap = defSrcMap

if sourceMap
source = sourceMap.sourceLocation line - 1, column - 1
fileLocation = helpers.locationDataToString source if source

fileLocation = "#{fileName||'<anonymous>'}:#{line}:#{column}" if not fileLocation

functionName = frame.getFunctionName()
isConstructor = frame.isConstructor()
Expand Down Expand Up @@ -314,14 +279,9 @@ getSourceMap = (filename) ->
# sourceMap, so we must monkey-patch Error to display CoffeeScript source
# positions.
Error.prepareStackTrace = (err, stack) ->
getSourceMapping = (filename, line, column) ->
sourceMap = getSourceMap 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}"
break if frame.getFunction()?.stopStackTrace
" at #{formatSourcePosition frame, err.srcMap}"

"#{err.name}: #{err.message ? ''}\n#{frames.join '\n'}\n"

6 changes: 4 additions & 2 deletions src/command.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ BANNER = '''

# The list of all the valid option flags that `coffee` knows how to handle.
SWITCHES = [
['-a', '--macro', 'enable macro support']
['-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']
Expand Down Expand Up @@ -319,6 +320,7 @@ compileOptions = (filename, base) ->
filename
literate: opts.literate or helpers.isLiterate(filename)
bare: opts.bare
macro: opts.macro
header: opts.compile
sourceMap: opts.map
}
Expand All @@ -330,13 +332,13 @@ compileOptions = (filename, base) ->
answer = helpers.merge answer, {
jsPath
sourceRoot: path.relative jsDir, cwd
sourceFiles: [path.relative cwd, filename]
filename: path.relative cwd, filename
generatedFile: helpers.baseFileName(jsPath, no, useWinPathSep)
}
else
answer = helpers.merge answer,
sourceRoot: ""
sourceFiles: [helpers.baseFileName filename, no, useWinPathSep]
filename: helpers.baseFileName filename, no, useWinPathSep
generatedFile: helpers.baseFileName(filename, yes, useWinPathSep) + ".js"
answer

Expand Down
66 changes: 30 additions & 36 deletions src/helpers.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@ exports.repeat = repeat = (str, n) ->
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 `Base#compile` is called, to allow properties in the
# options hash to propagate down the tree without polluting other branches.
Expand Down Expand Up @@ -94,6 +87,7 @@ buildLocationData = (first, last) ->
first_column: first.first_column
last_line: last.last_line
last_column: last.last_column
file_num: last.file_num

# This returns a function which takes an object as a parameter, and if that
# object is an AST node, updates that object's locationData.
Expand All @@ -107,15 +101,12 @@ exports.addLocationDataFn = (first, last) ->

# 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"
exports.locationDataToString = (ld) ->
return '<unknown location>' if not ld
loc = ''
if filename = filenames[ld.file_num ? filenames.length-1]
loc += filename+':'
loc += (ld.first_line + 1) + ':' + (ld.first_column + 1)

# A `.coffee.md` compatible version of `basename`, that returns the file sans-extension.
exports.baseFileName = (file, stripExt = no, useWinPathSep = no) ->
Expand All @@ -139,40 +130,43 @@ exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file
# format <filename>:<line>:<col>: <message> plus the line with the error and a
# marker showing where the error is.
exports.throwSyntaxError = (message, location) ->
error = new SyntaxError message
error.location = location
error.toString = syntaxErrorToString

err = new SyntaxError message
err.location = location
# Instead of showing the compiler's stacktrace, show our custom error message
# (this is useful when the error bubbles up in Node.js applications that
# compile CoffeeScript for example).
error.stack = error.toString()
err.toString = syntaxErrorToString
err.stack = err.toString()
throw err


# Lists of coffeescript sources and filenames, for every bit of code that is
# tokenized. It's indexed by `file_num`, which is set on each of the parser
# nodes and is passed to the lexer.
exports.scripts = scripts = []
exports.filenames = filenames = []

throw error
exports.getFileNum = (source, filename) ->
fileNum = scripts.length
scripts[fileNum] = source
filenames[fileNum] = filename
fileNum

# Update a compiler SyntaxError with source code information if it didn't have
# it already.
exports.updateSyntaxError = (error, code, filename) ->
# Avoid screwing up the `stack` property of other errors (i.e. possible bugs).
if error.toString is syntaxErrorToString
error.code or= code
error.filename or= filename
error.stack = error.toString()
error

syntaxErrorToString = ->
return Error::toString.call @ unless @code and @location
return Error::toString.call @ unless @location

{first_line, first_column, last_line, last_column} = @location
{first_line, first_column, last_line, last_column, file_num} = @location
file_num ?= scripts.length-1 # we're parsing/lexing the most recent script
last_line ?= first_line
last_column ?= first_column

filename = @filename or '[stdin]'
codeLine = @code.split('\n')[first_line]
filename = filenames[file_num] or '[stdin]'
codeLine = scripts[file_num]?.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)
marker = codeLine.substr(0,start).replace(/[^\t]/g,' ') + codeLine.substring(start,end).replace(/[^\t]/g,'^')

# Check to see if we're running on a color-enabled TTY.
if process?
Expand Down
6 changes: 3 additions & 3 deletions src/lexer.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
{Rewriter, INVERSES} = require './rewriter'

# Import the helpers we need.
{count, starts, compact, last, repeat, invertLiterate,
locationDataToString, throwSyntaxError} = require './helpers'
{starts, compact, last, repeat, invertLiterate, throwSyntaxError} = require './helpers'

# The Lexer Class
# ---------------
Expand Down Expand Up @@ -631,7 +630,8 @@ exports.Lexer = class Lexer
else
string = @chunk[..offset-1]

lineCount = count string, '\n'
lineCount = pos = 0
lineCount++ while pos = 1 + string.indexOf "\n", pos

column = @chunkColumn
if lineCount > 0
Expand Down
Loading