aboutsummaryrefslogtreecommitdiff
path: root/nomsu_environment.moon
blob: 6ae57d2be11696f9b446f0843a778e376b07e97f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
-- This file defines the environment in which Nomsu code runs, including some
-- basic bootstrapping functionality.
{:NomsuCode, :LuaCode, :Source} = require "code_obj"
{:Importer, :import_to_1_from, :_1_forked} = require 'importer'
{:List, :Dict, :Text} = require 'containers'
SyntaxTree = require "syntax_tree"
Files = require "files"
make_parser = require("parser")
pretty_error = require("pretty_errors")

make_tree = (tree, userdata)->
    tree.source = Source(userdata.filename, tree.start, tree.stop)
    tree.start, tree.stop = nil, nil
    tree = SyntaxTree(tree)
    return tree

Parsers = {}
max_parser_version = 0
for version=1,999
    local peg_file
    if package.nomsupath
        for path in package.nomsupath\gmatch("[^;]+")
            continue if path == "." and package.nomsupath != "."
            peg_file = io.open(path.."/nomsu.#{version}.peg")
            break if peg_file
    else
        peg_file = io.open("nomsu.#{version}.peg")
    break unless peg_file
    max_parser_version = version
    peg_contents = peg_file\read('*a')
    peg_file\close!
    Parsers[version] = make_parser(peg_contents, make_tree)

{:tree_to_nomsu, :tree_to_inline_nomsu} = require "nomsu_decompiler"
{:compile, :compile_error} = require('nomsu_compiler')
_currently_running_files = List{} -- Used to check for circular imports in run_file_1_in
nomsu_environment = Importer{
    NOMSU_COMPILER_VERSION: 12, NOMSU_SYNTAX_VERSION: max_parser_version
    -- Lua stuff:
    :next, unpack: unpack or table.unpack, :setmetatable, :rawequal, :getmetatable, :pcall,
    yield:coroutine.yield, resume:coroutine.resume, coroutine_status_of:coroutine.status,
    coroutine_wrap:coroutine.wrap, coroutine_from: coroutine.create,
    :error, :package, :os, :require, :tonumber, :tostring, :string, :xpcall, :module,
    say:print, :loadfile, :rawset, :_VERSION, :collectgarbage, :rawget, :rawlen,
    :table, :assert, :dofile, :loadstring, lua_type_of:type, :select, :math, :io, :load,
    :pairs, :ipairs, :jit, :_VERSION
    bit: (jit or _VERSION == "Lua 5.2") and require('bitops') or nil
    -- Nomsu types:
    List:List, Dict:Dict,
    -- Utilities and misc.
    lpeg:lpeg, re:re, Files:Files,
    :SyntaxTree, TESTS: Dict({}), globals: Dict({}),
    :LuaCode, :NomsuCode, :Source
    LuaCode_from: ((src, ...)-> LuaCode\from(src, ...)),
    NomsuCode_from: ((src, ...)-> NomsuCode\from(src, ...)),
    SOURCE_MAP: Importer({})

    -- Nomsu functions:
    _1_as_nomsu:tree_to_nomsu, _1_as_inline_nomsu:tree_to_inline_nomsu,
    compile: compile, _1_as_lua: compile, compile_error_at:compile_error,
    :_1_forked, :import_to_1_from, exit:os.exit, quit:os.exit,

    _1_parsed: (nomsu_code, syntax_version)->
        if type(nomsu_code) == 'string'
            filename = Files.spoof(nomsu_code)
            nomsu_code = NomsuCode\from(Source(filename, 1, #nomsu_code), nomsu_code)
        source = nomsu_code.source
        nomsu_code = tostring(nomsu_code)
        version = nomsu_code\match("^#![^\n]*nomsu[ ]+-V[ ]*([0-9.]+)")
        if syntax_version
            syntax_version = tonumber(syntax_version\match("^[0-9]+"))
        syntax_version or= version and tonumber(version\match("^[0-9]+")) or max_parser_version
        parse = Parsers[syntax_version] or Parsers[max_parser_version]
        tree = parse(nomsu_code, source.filename)
        if tree.shebang
            tree.version or= tree.shebang\match("nomsu %-V[ ]*([%d.]*)")
        errs = {}
        find_errors = (t)->
            if t.type == "Error"
                errs[#errs+1] = t
            else
                for k,v in pairs(t)
                    continue unless SyntaxTree\is_instance(v)
                    find_errors(v)
        find_errors(tree)
        num_errs = #errs
        if num_errs > 0
            err_strings = [pretty_error{
                    title:"Parse error"
                    error:e.error, hint:e.hint, source:e\get_source_file!
                    start:e.source.start, stop:e.source.stop, filename:e.source.filename
                } for i, e in ipairs(errs) when i <= 3]
            if num_errs > #err_strings
                table.insert(err_strings, "\027[31;1m +#{num_errs-#err_strings} additional errors...\027[0m\n")
            error(table.concat(err_strings, '\n\n'), 0)
        
        return tree

    run_1_in: (to_run, environment)->
        if type(to_run) == 'string'
            filename = Files.spoof(to_run)
            to_run = NomsuCode\from(Source(filename, 1, #to_run), to_run)
            ret = environment.run_1_in(to_run, environment)
            return ret
        elseif NomsuCode\is_instance(to_run)
            tree = environment._1_parsed(to_run)
            return nil if tree == nil
            ret = environment.run_1_in(tree, environment)
            return ret
        elseif SyntaxTree\is_instance(to_run)
            filename = to_run.source.filename\gsub("\n.*", "...")
            if to_run.type != "FileChunks"
                to_run = {to_run}
            -- Each chunk's compilation is affected by the code in the previous chunks
            -- (typically), so each chunk needs to compile and run before the next one
            -- compiles.
            ret = nil
            for chunk_no, chunk in ipairs to_run
                lua = environment.compile(chunk)
                lua\declare_locals!
                lua\prepend "-- File: #{filename} chunk ##{chunk_no}\n"
                ret = environment.run_1_in(lua, environment)
            return ret
        elseif LuaCode\is_instance(to_run)
            source = to_run.source
            lua_string = to_run\text!
            -- If you replace tostring(source) with "nil", source mapping won't happen
            run_lua_fn, err = load(lua_string, tostring(source), "t", environment)
            if not run_lua_fn
                lines =[("%3d|%s")\format(i,line) for i, line in ipairs Files.get_lines(lua_string)]
                line_numbered_lua = table.concat(lines, "\n")
                error("Failed to compile generated code:\n\027[1;34m#{line_numbered_lua}\027[0m\n\n#{err}", 0)
            source_key = tostring(source)
            unless environment.SOURCE_MAP[source_key] or environment.OPTIMIZATION >= 2
                map = {}
                file = Files.read(source.filename)
                if not file
                    error "Failed to find file: #{source.filename}"
                nomsu_str = file\sub(source.start, source.stop)
                lua_line = 1
                nomsu_line = Files.get_line_number(file, source.start)
                map_sources = (s)->
                    if type(s) == 'string'
                        for nl in s\gmatch("\n")
                            map[lua_line] or= nomsu_line
                            lua_line += 1
                    else
                        if s.source and s.source.filename == source.filename
                            nomsu_line = Files.get_line_number(file, s.source.start)
                        for b in *s.bits do map_sources(b)
                map_sources(to_run)
                map[lua_line] or= nomsu_line
                map[0] = 0
                -- Mapping from lua line number to nomsu line numbers
                environment.SOURCE_MAP[source_key] = map
            return run_lua_fn!
        else
            error("Attempt to run unknown thing: "..tostring(to_run))
    
    FILE_CACHE: {}
    run_file_1_in: (path, environment, optimization, prefix=nil)->
        if not optimization
            optimization = environment.OPTIMIZATION
        if environment.FILE_CACHE[path]
            import_to_1_from(environment, environment.FILE_CACHE[path], prefix)
            return
        if _currently_running_files\has(path)
            i = _currently_running_files\index_of(path)
            _currently_running_files\add path
            circle = _currently_running_files\from_1_to(i, -1)
            error("Circular import detected:\n           "..circle\joined_with("\n..imports  "))
        _currently_running_files\add path
        mod = _1_forked(environment)

        did_anything = false
        for nomsupath in package.nomsupath\gmatch("[^;]+")
            full_path = nomsupath == "." and path or nomsupath.."/"..path
            files = Files.list(full_path)
            continue unless files
            for filename in *files
                lua_filename = filename\gsub("%.nom$", ".lua")
                -- Need to check here to prevent re-running files
                if environment.FILE_CACHE[filename]
                    import_to_1_from(environment, environment.FILE_CACHE[filename], prefix)
                    did_anything = true
                    continue
                -- TODO: don't automatically use precompiled version?
                code = if optimization != 0 and Files.exists(lua_filename)
                    -- TODO: use a checksum?
                    file = Files.read(lua_filename)
                    LuaCode\from(Source(filename, 1, #file), file)
                else
                    file = Files.read(filename)
                    NomsuCode\from(Source(filename, 1, #file), file)
                environment.run_1_in(code, mod)
                did_anything = true
            break
        unless did_anything
            error("File not found: #{path}\n(searched in #{package.nomsupath})", 0)
        import_to_1_from(environment, mod, prefix)
        environment.FILE_CACHE[path] = mod
        _currently_running_files\remove!
}
nomsu_environment._ENV = nomsu_environment

-- Hacky use of globals:
export SOURCE_MAP
SOURCE_MAP = nomsu_environment.SOURCE_MAP

return nomsu_environment