diff --git a/lib/linguist/languages.yml b/lib/linguist/languages.yml index 9d4eefae0b..62a9e651b3 100644 --- a/lib/linguist/languages.yml +++ b/lib/linguist/languages.yml @@ -848,6 +848,10 @@ Moocode: extensions: - .moo +MoonScript: + type: programming + primary_extension: .moon + Myghty: primary_extension: .myt extensions: diff --git a/samples/MoonScript/transform.moon b/samples/MoonScript/transform.moon new file mode 100644 index 0000000000..22c6b668be --- /dev/null +++ b/samples/MoonScript/transform.moon @@ -0,0 +1,903 @@ + +types = require "moonscript.types" +util = require "moonscript.util" +data = require "moonscript.data" + +import reversed, unpack from util +import ntype, mtype, build, smart_node, is_slice, value_is_singular from types +import insert from table +import NameProxy, LocalName from require "moonscript.transform.names" + +destructure = require "moonscript.transform.destructure" + +local implicitly_return + +class Run + new: (@fn) => + self[1] = "run" + + call: (state) => + self.fn state + +-- transform the last stm is a list of stms +-- will puke on group +apply_to_last = (stms, fn) -> + -- find last (real) exp + last_exp_id = 0 + for i = #stms, 1, -1 + stm = stms[i] + if stm and mtype(stm) != Run + last_exp_id = i + break + + return for i, stm in ipairs stms + if i == last_exp_id + fn stm + else + stm + +-- is a body a sindle expression/statement +is_singular = (body) -> + return false if #body != 1 + if "group" == ntype body + is_singular body[2] + else + true + +find_assigns = (body, out={}) -> + for thing in *body + switch thing[1] + when "group" + find_assigns thing[2], out + when "assign" + table.insert out, thing[2] -- extract names + out + +hoist_declarations = (body) -> + assigns = {} + + -- hoist the plain old assigns + for names in *find_assigns body + for name in *names + table.insert assigns, name if type(name) == "string" + + -- insert after runs + idx = 1 + while mtype(body[idx]) == Run do idx += 1 + + table.insert body, idx, {"declare", assigns} + +expand_elseif_assign = (ifstm) -> + for i = 4, #ifstm + case = ifstm[i] + if ntype(case) == "elseif" and ntype(case[2]) == "assign" + split = { unpack ifstm, 1, i - 1 } + insert split, { + "else", { + {"if", case[2], case[3], unpack ifstm, i + 1} + } + } + return split + + ifstm + +constructor_name = "new" + +with_continue_listener = (body) -> + continue_name = nil + { + Run => + @listen "continue", -> + unless continue_name + continue_name = NameProxy"continue" + @put_name continue_name + continue_name + + build.group body + + Run => + return unless continue_name + @put_name continue_name, nil + @splice (lines) -> { + {"assign", {continue_name}, {"false"}} + {"repeat", "true", { + lines + {"assign", {continue_name}, {"true"}} + }} + {"if", {"not", continue_name}, { + {"break"} + }} + } + } + + +class Transformer + new: (@transformers) => + @seen_nodes = setmetatable {}, __mode: "k" + + transform: (scope, node, ...) => + return node if @seen_nodes[node] + @seen_nodes[node] = true + while true + transformer = @transformers[ntype node] + res = if transformer + transformer(scope, node, ...) or node + else + node + return node if res == node + node = res + node + + bind: (scope) => + (...) -> @transform scope, ... + + __call: (...) => @transform ... + + can_transform: (node) => + @transformers[ntype node] != nil + +construct_comprehension = (inner, clauses) -> + current_stms = inner + for _, clause in reversed clauses + t = clause[1] + current_stms = if t == "for" + _, names, iter = unpack clause + {"foreach", names, {iter}, current_stms} + elseif t == "when" + _, cond = unpack clause + {"if", cond, current_stms} + else + error "Unknown comprehension clause: "..t + current_stms = {current_stms} + + current_stms[1] + +Statement = Transformer { + root_stms: (body) => + apply_to_last body, implicitly_return @ + + assign: (node) => + names, values = unpack node, 2 + -- bubble cascading assigns + transformed = if #values == 1 + value = values[1] + t = ntype value + + if t == "decorated" + value = @transform.statement value + t = ntype value + + if types.cascading[t] + ret = (stm) -> + if types.is_value stm + {"assign", names, {stm}} + else + stm + + build.group { + {"declare", names} + @transform.statement value, ret, node + } + + node = transformed or node + + if destructure.has_destructure names + return destructure.split_assign node + + node + + continue: (node) => + continue_name = @send "continue" + error "continue must be inside of a loop" unless continue_name + build.group { + build.assign_one continue_name, "true" + {"break"} + } + + export: (node) => + -- assign values if they are included + if #node > 2 + if node[2] == "class" + cls = smart_node node[3] + build.group { + {"export", {cls.name}} + cls + } + else + build.group { + node + build.assign { + names: node[2] + values: node[3] + } + } + else + nil + + update: (node) => + _, name, op, exp = unpack node + op_final = op\match "^(.+)=$" + error "Unknown op: "..op if not op_final + exp = {"parens", exp} unless value_is_singular exp + build.assign_one name, {"exp", name, op_final, exp} + + import: (node) => + _, names, source = unpack node + + stubs = for name in *names + if type(name) == "table" + name + else + {"dot", name} + + real_names = for name in *names + type(name) == "table" and name[2] or name + + if type(source) == "string" + build.assign { + names: real_names + values: [build.chain { base: source, stub} for stub in *stubs] + } + else + source_name = NameProxy "table" + build.group { + {"declare", real_names} + build["do"] { + build.assign_one source_name, source + build.assign { + names: real_names + values: [build.chain { base: source_name, stub} for stub in *stubs] + } + } + } + + comprehension: (node, action) => + _, exp, clauses = unpack node + + action = action or (exp) -> {exp} + construct_comprehension action(exp), clauses + + do: (node, ret) => + node[2] = apply_to_last node[2], ret if ret + node + + decorated: (node) => + stm, dec = unpack node, 2 + + wrapped = switch dec[1] + when "if" + cond, fail = unpack dec, 2 + fail = { "else", { fail } } if fail + { "if", cond, { stm }, fail } + when "unless" + { "unless", dec[2], { stm } } + when "comprehension" + { "comprehension", stm, dec[2] } + else + error "Unknown decorator " .. dec[1] + + if ntype(stm) == "assign" + wrapped = build.group { + build.declare names: [name for name in *stm[2] when type(name) == "string"] + wrapped + } + + wrapped + + unless: (node) => + { "if", {"not", {"parens", node[2]}}, unpack node, 3 } + + if: (node, ret) => + -- expand assign in cond + if ntype(node[2]) == "assign" + _, assign, body = unpack node + if destructure.has_destructure assign[2] + name = NameProxy "des" + + body = { + destructure.build_assign assign[2][1], name + build.group node[3] + } + + return build.do { + build.assign_one name, assign[3][1] + {"if", name, body, unpack node, 4} + } + else + name = assign[2][1] + return build["do"] { + assign + {"if", name, unpack node, 3} + } + + node = expand_elseif_assign node + + -- apply cascading return decorator + if ret + smart_node node + -- mutate all the bodies + node['then'] = apply_to_last node['then'], ret + for i = 4, #node + case = node[i] + body_idx = #node[i] + case[body_idx] = apply_to_last case[body_idx], ret + + node + + with: (node, ret) => + _, exp, block = unpack node + + scope_name = NameProxy "with" + + named_assign = if ntype(exp) == "assign" + names, values = unpack exp, 2 + assign_name = names[1] + exp = values[1] + values[1] = scope_name + {"assign", names, values} + + build.do { + Run => @set "scope_var", scope_name + build.assign_one scope_name, exp + build.group { named_assign } + build.group block + + if ret + ret scope_name + } + + foreach: (node) => + smart_node node + source = unpack node.iter + + destructures = {} + node.names = for i, name in ipairs node.names + if ntype(name) == "table" + with proxy = NameProxy "des" + insert destructures, destructure.build_assign name, proxy + else + name + + if next destructures + insert destructures, build.group node.body + node.body = destructures + + if ntype(source) == "unpack" + list = source[2] + + index_name = NameProxy "index" + list_name = NameProxy "list" + + slice_var = nil + bounds = if is_slice list + slice = list[#list] + table.remove list + table.remove slice, 1 + + slice[2] = if slice[2] and slice[2] != "" + max_tmp_name = NameProxy "max" + slice_var = build.assign_one max_tmp_name, slice[2] + {"exp", max_tmp_name, "<", 0 + "and", {"length", list_name}, "+", max_tmp_name + "or", max_tmp_name } + else + {"length", list_name} + + slice + else + {1, {"length", list_name}} + + return build.group { + build.assign_one list_name, list + slice_var + build["for"] { + name: index_name + bounds: bounds + body: { + {"assign", node.names, {list_name\index index_name}} + build.group node.body + } + } + } + + node.body = with_continue_listener node.body + + while: (node) => + smart_node node + node.body = with_continue_listener node.body + + for: (node) => + smart_node node + node.body = with_continue_listener node.body + + switch: (node, ret) => + _, exp, conds = unpack node + exp_name = NameProxy "exp" + + -- convert switch conds into if statment conds + convert_cond = (cond) -> + t, case_exps, body = unpack cond + out = {} + insert out, t == "case" and "elseif" or "else" + if t != "else" + cond_exp = {} + for i, case in ipairs case_exps + if i == 1 + insert cond_exp, "exp" + else + insert cond_exp, "or" + + case = {"parens", case} unless value_is_singular case + insert cond_exp, {"exp", case, "==", exp_name} + + insert out, cond_exp + else + body = case_exps + + if ret + body = apply_to_last body, ret + + insert out, body + + out + + first = true + if_stm = {"if"} + for cond in *conds + if_cond = convert_cond cond + if first + first = false + insert if_stm, if_cond[2] + insert if_stm, if_cond[3] + else + insert if_stm, if_cond + + build.group { + build.assign_one exp_name, exp + if_stm + } + + class: (node, ret, parent_assign) => + _, name, parent_val, body = unpack node + + -- split apart properties and statements + statements = {} + properties = {} + for item in *body + switch item[1] + when "stm" + insert statements, item[2] + when "props" + for tuple in *item[2,] + if ntype(tuple[1]) == "self" + insert statements, build.assign_one unpack tuple + else + insert properties, tuple + + -- find constructor + constructor = nil + properties = for tuple in *properties + key = tuple[1] + if key[1] == "key_literal" and key[2] == constructor_name + constructor = tuple[2] + nil + else + tuple + + parent_cls_name = NameProxy "parent" + base_name = NameProxy "base" + self_name = NameProxy "self" + cls_name = NameProxy "class" + + if not constructor + constructor = build.fndef { + args: {{"..."}} + arrow: "fat" + body: { + build["if"] { + cond: parent_cls_name + then: { + build.chain { base: "super", {"call", {"..."}} } + } + } + } + } + else + smart_node constructor + constructor.arrow = "fat" + + real_name = name or parent_assign and parent_assign[2][1] + real_name = switch ntype real_name + when "chain" + last = real_name[#real_name] + switch ntype last + when "dot" + {"string", '"', last[2]} + when "index" + last[2] + else + "nil" + when "nil" + "nil" + else + {"string", '"', real_name} + + cls = build.table { + {"__init", constructor} + {"__base", base_name} + {"__name", real_name} -- "quote the string" + {"__parent", parent_cls_name} + } + + -- look up a name in the class object + class_lookup = build["if"] { + cond: {"exp", "val", "==", "nil", "and", parent_cls_name} + then: { + parent_cls_name\index"name" + } + } + insert class_lookup, {"else", {"val"}} + + cls_mt = build.table { + {"__index", build.fndef { + args: {{"cls"}, {"name"}} + body: { + build.assign_one LocalName"val", build.chain { + base: "rawget", {"call", {base_name, "name"}} + } + class_lookup + } + }} + {"__call", build.fndef { + args: {{"cls"}, {"..."}} + body: { + build.assign_one self_name, build.chain { + base: "setmetatable" + {"call", {"{}", base_name}} + } + build.chain { + base: "cls.__init" + {"call", {self_name, "..."}} + } + self_name + } + }} + } + + cls = build.chain { + base: "setmetatable" + {"call", {cls, cls_mt}} + } + + value = nil + with build + out_body = { + Run => + -- make sure we don't assign the class to a local inside the do + @put_name name if name + + @set "super", (block, chain) -> + if chain + slice = [item for item in *chain[3,]] + new_chain = {"chain", parent_cls_name} + + head = slice[1] + + if head == nil + return parent_cls_name + + switch head[1] + -- calling super, inject calling name and self into chain + when "call" + calling_name = block\get"current_block" + slice[1] = {"call", {"self", unpack head[2]}} + + if ntype(calling_name) == "key_literal" + insert new_chain, {"dot", calling_name[2]} + else + insert new_chain, {"index", calling_name} + + -- colon call on super, replace class with self as first arg + when "colon" + call = head[3] + insert new_chain, {"dot", head[2]} + slice[1] = { "call", { "self", unpack call[2] } } + + insert new_chain, item for item in *slice + + new_chain + else + parent_cls_name + + .assign_one parent_cls_name, parent_val == "" and "nil" or parent_val + .assign_one base_name, {"table", properties} + .assign_one base_name\chain"__index", base_name + + .if { + cond: parent_cls_name + then: { + .chain { + base: "setmetatable" + {"call", { + base_name, + .chain { base: parent_cls_name, {"dot", "__base"}} + }} + } + } + } + + .assign_one cls_name, cls + .assign_one base_name\chain"__class", cls_name + + .group if #statements > 0 then { + .assign_one LocalName"self", cls_name + .group statements + } + + -- run the inherited callback + .if { + cond: {"exp", + parent_cls_name, "and", parent_cls_name\chain "__inherited" + } + then: { + parent_cls_name\chain "__inherited", {"call", { + parent_cls_name, cls_name + }} + } + } + + .group if name then { + .assign_one name, cls_name + } + + if ret + ret cls_name + } + + hoist_declarations out_body + + value = .group { + .group if ntype(name) == "value" then { + .declare names: {name} + } + + .do out_body + } + + value +} + +class Accumulator + body_idx: { for: 4, while: 3, foreach: 4 } + + new: => + @accum_name = NameProxy "accum" + @value_name = NameProxy "value" + @len_name = NameProxy "len" + + -- wraps node and mutates body + convert: (node) => + index = @body_idx[ntype node] + node[index] = @mutate_body node[index] + @wrap node + + -- wrap the node into a block_exp + wrap: (node) => + build.block_exp { + build.assign_one @accum_name, build.table! + build.assign_one @len_name, 0 + node + @accum_name + } + + -- mutates the body of a loop construct to save last value into accumulator + -- can optionally skip nil results + mutate_body: (body, skip_nil=true) => + val = if not skip_nil and is_singular body + with body[1] + body = {} + else + body = apply_to_last body, (n) -> + if types.is_value n + build.assign_one @value_name, n + else + -- just ignore it + build.group { + {"declare", {@value_name}} + n + } + @value_name + + update = { + {"update", @len_name, "+=", 1} + build.assign_one @accum_name\index(@len_name), val + } + + if skip_nil + table.insert body, build["if"] { + cond: {"exp", @value_name, "!=", "nil"} + then: update + } + else + table.insert body, build.group update + + body + +default_accumulator = (node) => + Accumulator!\convert node + +implicitly_return = (scope) -> + is_top = true + fn = (stm) -> + t = ntype stm + + -- expand decorated + if t == "decorated" + stm = scope.transform.statement stm + t = ntype stm + + if types.cascading[t] + is_top = false + scope.transform.statement stm, fn + elseif types.manual_return[t] or not types.is_value stm + -- remove blank return statement + if is_top and t == "return" and stm[2] == "" + nil + else + stm + else + if t == "comprehension" and not types.comprehension_has_value stm + stm + else + {"return", stm} + + fn + +Value = Transformer { + for: default_accumulator + while: default_accumulator + foreach: default_accumulator + + do: (node) => + build.block_exp node[2] + + decorated: (node) => + @transform.statement node + + class: (node) => + build.block_exp { node } + + string: (node) => + delim = node[2] + + convert_part = (part) -> + if type(part) == "string" or part == nil + {"string", delim, part or ""} + else + build.chain { base: "tostring", {"call", {part[2]}} } + + -- reduced to single item + if #node <= 3 + return if type(node[3]) == "string" + node + else + convert_part node[3] + + e = {"exp", convert_part node[3]} + + for i=4, #node + insert e, ".." + insert e, convert_part node[i] + e + + comprehension: (node) => + a = Accumulator! + node = @transform.statement node, (exp) -> + a\mutate_body {exp}, false + a\wrap node + + tblcomprehension: (node) => + _, explist, clauses = unpack node + key_exp, value_exp = unpack explist + + accum = NameProxy "tbl" + + inner = if value_exp + dest = build.chain { base: accum, {"index", key_exp} } + { build.assign_one dest, value_exp } + else + -- If we only have single expression then + -- unpack the result into key and value + key_name, val_name = NameProxy"key", NameProxy"val" + dest = build.chain { base: accum, {"index", key_name} } + { + build.assign names: {key_name, val_name}, values: {key_exp} + build.assign_one dest, val_name + } + + build.block_exp { + build.assign_one accum, build.table! + construct_comprehension inner, clauses + accum + } + + fndef: (node) => + smart_node node + node.body = apply_to_last node.body, implicitly_return self + node.body = { + Run => @listen "varargs", -> -- capture event + unpack node.body + } + + node + + if: (node) => build.block_exp { node } + unless: (node) =>build.block_exp { node } + with: (node) => build.block_exp { node } + switch: (node) => + build.block_exp { node } + + -- pull out colon chain + chain: (node) => + stub = node[#node] + + -- escape lua keywords used in dot accessors + for i=3,#node + part = node[i] + if ntype(part) == "dot" and data.lua_keywords[part[2]] + node[i] = { "index", {"string", '"', part[2]} } + + if ntype(node[2]) == "string" + -- add parens if callee is raw string + node[2] = {"parens", node[2] } + elseif type(stub) == "table" and stub[1] == "colon_stub" + -- convert colon stub into code + table.remove node, #node + + base_name = NameProxy "base" + fn_name = NameProxy "fn" + + is_super = node[2] == "super" + @transform.value build.block_exp { + build.assign { + names: {base_name} + values: {node} + } + + build.assign { + names: {fn_name} + values: { + build.chain { base: base_name, {"dot", stub[2]} } + } + } + + build.fndef { + args: {{"..."}} + body: { + build.chain { + base: fn_name, {"call", {is_super and "self" or base_name, "..."}} + } + } + } + } + + block_exp: (node) => + _, body = unpack node + + fn = nil + arg_list = {} + + fn = smart_node build.fndef body: { + Run => + @listen "varargs", -> + insert arg_list, "..." + insert fn.args, {"..."} + @unlisten "varargs" + + unpack body + } + + build.chain { base: {"parens", fn}, {"call", arg_list} } +} + +{ :Statement, :Value, :Run }