diff --git a/Slakefile b/Slakefile index facec06f0..3afa5ad99 100644 --- a/Slakefile +++ b/Slakefile @@ -138,7 +138,13 @@ function runTests global.LiveScript else tint message if failedTests process.exit 1 - dir(\test)forEach (file) -> + + files = dir \test + unless '--harmony' in process.execArgv or '--harmony-generators' in process.execArgv + say "Skipping --harmony tests" + files.splice (files.indexOf 'generators.ls'), 1 + + files.forEach (file) -> return unless /\.ls$/i.test file code = slurp filename = path.join \test file try LiveScript.run code, {filename} catch diff --git a/lib/ast.js b/lib/ast.js index a5ae7b7fe..459d58156 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1660,7 +1660,10 @@ exports.Unary = Unary = (function(superclass){ if (this.post) { code += op; } else { - if ((op === 'new' || op === 'typeof' || op === 'delete') || (op === '+' || op === '-') && op === code.charAt()) { + if (op === 'yieldfrom') { + op = 'yield* '; + } + if ((op === 'new' || op === 'typeof' || op === 'delete' || op === 'yield') || (op === '+' || op === '-') && op === code.charAt()) { op += ' '; } code = op + code; @@ -2568,13 +2571,14 @@ exports.Existence = Existence = (function(superclass){ }(Node, Negatable)); exports.Fun = Fun = (function(superclass){ var prototype = extend$((import$(Fun, superclass).displayName = 'Fun', Fun), superclass).prototype, constructor = Fun; - function Fun(params, body, bound, curried, hushed){ + function Fun(params, body, bound, curried, hushed, generator){ var this$ = this instanceof ctor$ ? this : new ctor$; this$.params = params || []; this$.body = body || Block(); this$.bound = bound && 'this$'; this$.curried = curried || false; this$.hushed = hushed != null ? hushed : false; + this$.generator = generator != null ? generator : false; return this$; } function ctor$(){} ctor$.prototype = prototype; prototype.children = ['params', 'body']; @@ -2622,6 +2626,11 @@ exports.Fun = Fun = (function(superclass){ o.indent += TAB; body = this.body, name = this.name, tab = this.tab; code = 'function'; + if (this.generator) { + this.ctor && this.carp("a constructor can't be a generator"); + this.hushed && this.carp("a generator is hushed by default"); + code += '*'; + } if (this.bound === 'this$') { if (this.ctor) { scope.assign('this$', 'this instanceof ctor$ ? this : new ctor$'); @@ -2641,7 +2650,7 @@ exports.Fun = Fun = (function(superclass){ if (this.statement || name && this.labeled) { code += ' ' + scope.add(name, 'function', this); } - this.hushed || this.ctor || this.newed || body.makeReturn(); + this.hushed || this.ctor || this.newed || this.generator || body.makeReturn(); code += "(" + this.compileParams(o, scope) + "){"; if (that = body.compileWithDeclarations(o)) { code += "\n" + that + "\n" + tab; diff --git a/lib/grammar.js b/lib/grammar.js index 1cfb42131..e303a1085 100644 --- a/lib/grammar.js +++ b/lib/grammar.js @@ -241,7 +241,7 @@ bnf = { }), o('Chain !?', function(){ return Existence($1.unwrap(), true); }), o('PARAM( ArgList OptComma )PARAM -> Block', function(){ - return L(Fun($2, $6, /~/.test($5), /--|~~/.test($5), /!/.test($5))); + return L(Fun($2, $6, /~/.test($5), /--|~~/.test($5), /!/.test($5), /\*/.test($5))); }), o('FUNCTION CALL( ArgList OptComma )CALL Block', function(){ return L(Fun($3, $6).named($1)); }), o('IF Expression Block Else', function(){ diff --git a/lib/lexer.js b/lib/lexer.js index 257643071..1188a1d63 100644 --- a/lib/lexer.js +++ b/lib/lexer.js @@ -144,6 +144,7 @@ exports.doID = function(code, index){ case 'do': case 'typeof': case 'delete': + case 'yield': tag = 'UNARY'; break; case 'return': @@ -312,6 +313,10 @@ exports.doID = function(code, index){ } break; case 'from': + if (last[1] === 'yield') { + last[1] += 'from'; + return 4; + } this.forange() && (tag = 'FROM'); break; case 'to': @@ -830,6 +835,12 @@ exports.doLiteral = function(code, index){ this.token('IMPORT', '<<'); return sym.length; case '*': + if (this.last[0] === '->') { + if (this.last[0] === '->') { + this.last[1] += '*'; + } + return 1; + } if (that = ((ref$ = this.last[0]) === 'NEWLINE' || ref$ === 'INDENT' || ref$ === 'THEN' || ref$ === '=>') && (INLINEDENT.lastIndex = index + 1, INLINEDENT).exec(code)[0].length) { this.tokens.push(['LITERAL', 'void', this.line], ['ASSIGN', '=', this.line]); this.indent(index + that - 1 - this.dent - code.lastIndexOf('\n', index - 1)); @@ -1772,8 +1783,8 @@ function indexOfPair(tokens, i){ } return -1; } -KEYWORDS_SHARED = ['true', 'false', 'null', 'this', 'void', 'super', 'return', 'throw', 'break', 'continue', 'if', 'else', 'for', 'while', 'switch', 'case', 'default', 'try', 'catch', 'finally', 'function', 'class', 'extends', 'implements', 'new', 'do', 'delete', 'typeof', 'in', 'instanceof', 'let', 'with', 'var', 'const', 'import', 'export', 'debugger']; -KEYWORDS_UNUSED = ['enum', 'interface', 'package', 'private', 'protected', 'public', 'static', 'yield']; +KEYWORDS_SHARED = ['true', 'false', 'null', 'this', 'void', 'super', 'return', 'throw', 'break', 'continue', 'if', 'else', 'for', 'while', 'switch', 'case', 'default', 'try', 'catch', 'finally', 'function', 'class', 'extends', 'implements', 'new', 'do', 'delete', 'typeof', 'in', 'instanceof', 'let', 'with', 'var', 'const', 'import', 'export', 'debugger', 'yield']; +KEYWORDS_UNUSED = ['enum', 'interface', 'package', 'private', 'protected', 'public', 'static']; JS_KEYWORDS = KEYWORDS_SHARED.concat(KEYWORDS_UNUSED); LS_KEYWORDS = ['xor', 'match', 'where']; ID = /((?!\s)[a-z_$\xAA-\uFFDC](?:(?!\s)[\w$\xAA-\uFFDC]|-[a-z])*)([^\n\S]*:(?![:=]))?|/ig; diff --git a/lib/parser.js b/lib/parser.js index b88182636..69264c9ef 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -3,7 +3,8 @@ yy: {}, symbols_: {"error":2,"Chain":3,"ID":4,"Parenthetical":5,"List":6,"STRNUM":7,"LITERAL":8,"DOT":9,"Key":10,"CALL(":11,"ArgList":12,"OptComma":13,")CALL":14,"?":15,"LET":16,"Block":17,"[":18,"Expression":19,"LoopHeads":20,"]":21,"DEDENT":22,"{":23,"}":24,"(":25,"BIOP":26,")":27,"BIOPR":28,"BIOPBP":29,"BIOPP":30,"PARAM(":31,")PARAM":32,"UNARY":33,"CREMENT":34,"BACKTICK":35,"TO":36,"BY":37,"WITH":38,"FOR":39,"Properties":40,"LABEL":41,"KeyBase":42,"Arg":43,",":44,"NEWLINE":45,"INDENT":46,"...":47,"Lines":48,"Line":49,"<-":50,"COMMENT":51,"REQUIRE":52,"CLONEPORT":53,"ASSIGN":54,"IMPORT":55,"+-":56,"CLONE":57,"COMPARE":58,"LOGIC":59,"MATH":60,"POWER":61,"SHIFT":62,"BITWISE":63,"CONCAT":64,"COMPOSE":65,"RELATION":66,"PIPE":67,"BACKPIPE":68,"!?":69,"->":70,"FUNCTION":71,"IF":72,"Else":73,"POST_IF":74,"LoopHead":75,"DO":76,"WHILE":77,"CASE":78,"HURL":79,"JUMP":80,"SWITCH":81,"Exprs":82,"Cases":83,"DEFAULT":84,"ELSE":85,"TRY":86,"CATCH":87,"FINALLY":88,"CLASS":89,"OptExtends":90,"OptImplements":91,"EXTENDS":92,"DECL":93,"KeyValue":94,"Property":95,":":96,"Body":97,"IN":98,"OF":99,"FROM":100,"IMPLEMENTS":101,"Root":102,"$accept":0,"$end":1}, terminals_: {2:"error",4:"ID",7:"STRNUM",8:"LITERAL",9:"DOT",11:"CALL(",14:")CALL",15:"?",16:"LET",18:"[",21:"]",22:"DEDENT",23:"{",24:"}",25:"(",26:"BIOP",27:")",28:"BIOPR",29:"BIOPBP",30:"BIOPP",31:"PARAM(",32:")PARAM",33:"UNARY",34:"CREMENT",35:"BACKTICK",36:"TO",37:"BY",38:"WITH",39:"FOR",41:"LABEL",44:",",45:"NEWLINE",46:"INDENT",47:"...",50:"<-",51:"COMMENT",52:"REQUIRE",53:"CLONEPORT",54:"ASSIGN",55:"IMPORT",56:"+-",57:"CLONE",58:"COMPARE",59:"LOGIC",60:"MATH",61:"POWER",62:"SHIFT",63:"BITWISE",64:"CONCAT",65:"COMPOSE",66:"RELATION",67:"PIPE",68:"BACKPIPE",69:"!?",70:"->",71:"FUNCTION",72:"IF",74:"POST_IF",76:"DO",77:"WHILE",78:"CASE",79:"HURL",80:"JUMP",81:"SWITCH",84:"DEFAULT",85:"ELSE",86:"TRY",87:"CATCH",88:"FINALLY",89:"CLASS",92:"EXTENDS",93:"DECL",96:":",98:"IN",99:"OF",100:"FROM",101:"IMPLEMENTS"}, productions_: [0,[3,1],[3,1],[3,1],[3,1],[3,1],[3,3],[3,3],[3,5],[3,2],[3,6],[3,4],[3,5],[3,7],[3,3],[3,4],[3,4],[3,3],[3,4],[3,4],[3,3],[3,7],[3,3],[3,7],[3,3],[3,3],[3,5],[3,6],[3,6],[3,5],[3,7],[3,4],[3,6],[3,7],[3,6],[3,6],[3,5],[3,3],[3,3],[6,4],[6,4],[6,5],[6,5],[10,1],[10,1],[42,1],[42,1],[12,0],[12,1],[12,3],[12,4],[12,6],[43,1],[43,2],[43,1],[13,0],[13,1],[48,0],[48,1],[48,3],[48,2],[49,1],[49,2],[49,6],[49,1],[49,1],[49,2],[17,3],[19,3],[19,3],[19,5],[19,1],[19,3],[19,6],[19,3],[19,6],[19,2],[19,2],[19,3],[19,3],[19,3],[19,2],[19,2],[19,2],[19,5],[19,3],[19,3],[19,3],[19,3],[19,3],[19,3],[19,3],[19,3],[19,3],[19,3],[19,3],[19,3],[19,2],[19,6],[19,6],[19,4],[19,3],[19,3],[19,4],[19,6],[19,2],[19,5],[19,1],[19,1],[19,2],[19,3],[19,5],[19,5],[19,2],[19,4],[19,4],[19,2],[19,2],[19,4],[19,6],[19,5],[19,7],[19,4],[19,5],[19,4],[19,3],[19,2],[19,2],[19,5],[82,1],[82,3],[94,1],[94,1],[94,3],[94,3],[94,5],[94,5],[95,3],[95,6],[95,1],[95,3],[95,3],[95,2],[95,2],[95,2],[95,1],[40,0],[40,1],[40,3],[40,4],[40,4],[5,3],[97,1],[97,1],[97,3],[73,0],[73,2],[73,5],[75,4],[75,6],[75,6],[75,8],[75,2],[75,4],[75,4],[75,6],[75,4],[75,6],[75,6],[75,8],[75,6],[75,5],[75,8],[75,7],[75,8],[75,10],[75,10],[75,2],[75,4],[75,4],[75,6],[20,1],[20,2],[20,3],[20,3],[83,3],[83,4],[90,2],[90,0],[91,2],[91,0],[102,1]], -performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$) { +performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$ +/**/) { var $0 = $$.length - 1; switch (yystate) { @@ -226,7 +227,7 @@ case 96:this.$ = yy.Block($$[$0-2]).pipe([$$[$0]], $$[$0-1]); break; case 97:this.$ = yy.Existence($$[$0-1].unwrap(), true); break; -case 98:this.$ = yy.L(yylineno, yy.Fun($$[$0-4], $$[$0], /~/.test($$[$0-1]), /--|~~/.test($$[$0-1]), /!/.test($$[$0-1]))); +case 98:this.$ = yy.L(yylineno, yy.Fun($$[$0-4], $$[$0], /~/.test($$[$0-1]), /--|~~/.test($$[$0-1]), /!/.test($$[$0-1]), /\*/.test($$[$0-1]))); break; case 99:this.$ = yy.L(yylineno, yy.Fun($$[$0-3], $$[$0]).named($$[$0-5])); break; diff --git a/package.json b/package.json index 4b76b8d43..707b5d141 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "scripts": { "pretest": "bin/slake build && bin/slake build:parser && bin/slake build", "test": "bin/slake test", + "test-harmony": "node --harmony ./bin/slake test", "posttest": "git checkout -- lib" }, "preferGlobal": true, diff --git a/package.json.ls b/package.json.ls index 65be7af7d..0c1e302a5 100644 --- a/package.json.ls +++ b/package.json.ls @@ -36,6 +36,7 @@ bin : scripts: pretest: "bin/slake build && bin/slake build:parser && bin/slake build" test: "bin/slake test" + \test-harmony : "node --harmony ./bin/slake test" posttest: "git checkout -- lib" preferGlobal: true diff --git a/src/ast.ls b/src/ast.ls index 2859968fb..4f383f0cc 100644 --- a/src/ast.ls +++ b/src/ast.ls @@ -1012,7 +1012,8 @@ class exports.Unary extends Node #{ it.compile o, LEVEL_LIST }).slice(8, -1)" code = it.compile o, LEVEL_OP + PREC.unary if @post then code += op else - op += ' ' if op in <[ new typeof delete ]> + op = 'yield* ' if op is \yieldfrom + op += ' ' if op in <[ new typeof delete yield ]> or op in <[ + - ]> and op is code.charAt! code = op + code if o.level < LEVEL_CALL then code else "(#code)" @@ -1591,7 +1592,7 @@ class exports.Existence extends Node implements Negatable #### Fun # A function definition. This is the only node that creates a `new Scope`. class exports.Fun extends Node - (@params or [], @body or Block!, @bound and \this$, @curried or false, @hushed = false) ~> + (@params or [], @body or Block!, @bound and \this$, @curried or false, @hushed = false, @generator = false) ~> children: <[ params body ]> @@ -1623,6 +1624,10 @@ class exports.Fun extends Node o.indent += TAB {body, name, tab} = this code = \function + if @generator + @ctor and @carp "a constructor can't be a generator" + @hushed and @carp "a generator is hushed by default" + code += \* if @bound is \this$ if @ctor scope.assign \this$ 'this instanceof ctor$ ? this : new ctor$' @@ -1637,7 +1642,7 @@ class exports.Fun extends Node pscope.add name, \function, this if @statement or name and @labeled code += ' ' + scope.add name, \function, this - @hushed or @ctor or @newed or body.makeReturn! + @hushed or @ctor or @newed or @generator or body.makeReturn! code += "(#{ @compileParams o, scope }){" code += "\n#that\n#tab" if body.compileWithDeclarations o code += \} diff --git a/src/grammar.ls b/src/grammar.ls index 1926560fa..5b0fc49be 100644 --- a/src/grammar.ls +++ b/src/grammar.ls @@ -245,7 +245,7 @@ bnf = # The function literal can be either anonymous with `->`, o 'PARAM( ArgList OptComma )PARAM -> Block' - , -> L Fun $2, $6, /~/.test($5), /--|~~/.test($5), /!/.test($5) + , -> L Fun $2, $6, /~/.test($5), /--|~~/.test($5), /!/.test($5), /\*/.test($5) # or named with `function`. o 'FUNCTION CALL( ArgList OptComma )CALL Block' -> L Fun($3, $6)named $1 diff --git a/src/lexer.ls b/src/lexer.ls index 181fb1158..683df1019 100644 --- a/src/lexer.ls +++ b/src/lexer.ls @@ -123,7 +123,7 @@ exports import switch id case <[ true false on off yes no null void arguments debugger ]> tag = \LITERAL - case \new \do \typeof \delete then tag = \UNARY + case <[ new do typeof delete yield ]> then tag = \UNARY case \return \throw then tag = \HURL case \break \continue then tag = \JUMP case \this \eval \super then return @token(\LITERAL id, true)length @@ -211,7 +211,11 @@ exports import or last.1 is \import and \All last.1 += that return 3 - case \from then @forange! and tag = \FROM + case \from + if last.1 is \yield + last.1 += \from + return 4 + @forange! and tag = \FROM case \to \til @forange! and @tokens.push [\FROM '' @line] [\STRNUM \0 @line] if @fget \from @@ -539,6 +543,9 @@ exports import @token \IMPORT \<< return sym.length case \* + if @last.0 is \-> + @last.1 += \* if @last.0 is \-> + return 1 if @last.0 in <[ NEWLINE INDENT THEN => ]> and (INLINEDENT <<< lastIndex: index+1)exec code .0.length @tokens.push [\LITERAL \void @line] [\ASSIGN \= @line] @@ -1215,13 +1222,13 @@ KEYWORDS_SHARED = <[ true false null this void super return throw break continue if else for while switch case default try catch finally function class extends implements new do delete typeof in instanceof - let with var const import export debugger + let with var const import export debugger yield ]> # The list of keywords that are reserved by JavaScript, but not used. # We throw a syntax error for these to avoid runtime errors. KEYWORDS_UNUSED = - <[ enum interface package private protected public static yield ]> + <[ enum interface package private protected public static ]> JS_KEYWORDS = KEYWORDS_SHARED ++ KEYWORDS_UNUSED diff --git a/test/compilation.ls b/test/compilation.ls index 68cdb2bf4..74b942259 100644 --- a/test/compilation.ls +++ b/test/compilation.ls @@ -197,5 +197,12 @@ eq "var a, b, c;\na = require('a');\nb = require('b');\nc = require('c');" bare # JS literal eq 'some js code!' bare '``some js code!``' +# generators +compileThrows "a constructor can't be a generator" 1 'class => ->*' +compileThrows "a generator is hushed by default" 1 '!->*' + +# https://github.com/jashkenas/coffee-script/pull/3240#issuecomment-38344281 +eq '(function*(){\n var body;\n body = (yield fn).body;\n});' bare '->* {body} = yield fn' + # [livescript#279](https://github.com/gkz/LiveScript/issues/279) -################################################################ +################################################################ \ No newline at end of file diff --git a/test/generators.ls b/test/generators.ls new file mode 100644 index 000000000..c09d2c33c --- /dev/null +++ b/test/generators.ls @@ -0,0 +1,68 @@ +# Generators +# ----------------- +# +# * Generator Definition + +# generator as argument +ok ->* 1 + +# named generator function +ok <| :fn ->* 2 + +# generator definition +x = ->* + yield 0 + yield 1 + yield 2 + +y = x! +z = y.next! +eq z.value, 0 +eq z.done, false +z = y.next! +eq z.value, 1 +eq z.done, false +z = y.next! +eq z.value, 2 +eq z.done, false +z = y.next! +eq z.value, void +eq z.done, true + +# yield from +first = ->* + i = 0 + loop => yield i++ +second = ->* + yield from first! +list = second! +for i to 3 + {value} = list.next! + eq value, i + +# curried bound generators +class A + val: 5 + curried: (x, y) ~~>* + yield @val + x + y +fn = (new A).curried +yield-add = fn 2 +y = yield-add 3 +z = y.next! +eq z.value, 10 +eq z.done, false +z = y.next! +eq z.value, void +eq z.done, true + +# bound generator +obj = + bound: -> + do ~>* + return this + unbound: -> + do ->* + return this + +eq obj, obj.bound().next().value +ok obj isnt obj.unbound().next().value \ No newline at end of file