-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathenv.ls
More file actions
163 lines (131 loc) · 5.85 KB
/
env.ls
File metadata and controls
163 lines (131 loc) · 5.85 KB
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
# This module defines how the compilation environment object behaves. These
# are the ones being passed around to macros and other functions involved in
# compilation. Environment objects most importantly store the macro table of a
# compilation scope.
{ keys, concat-map, unfoldr, map, reverse, fold } = require \prelude-ls
es-generate = require \escodegen .generate _
compile = require \./compile
# Recursively search a macro table and its parents for a macro with a given
# name. Returns `null` if unsuccessful; a macro representing the function if
# successful.
find-macro = (macro-table, name) ->
switch macro-table.contents[name]
| null => null # deliberately masks parent; fail
| undefined => # not defined at this level
if macro-table.parent
find-macro macro-table.parent, name # ask parent
else return null # no parent to ask; fail
| otherwise => that # defined at this level; succeed
flatten-macro-table = (table) ->
table
|> unfoldr -> [ it, it.parent ] if it # get chain of nested macro tables
|> map (.contents) # get their contents
|> reverse # they're backwards, so reverse
|> fold (<<<), {} # import each from oldest to newest
|> -> # wrap as expected
parent :
contents : it
parent : null
contents : {}
clone-array = (.slice 0)
find-root = ({parent}:macro-table) -> | parent => find-root that
| _ => macro-table
class env
(root-table=null, options={}) ~>
@macro-table = contents : {} parent : root-table
@root-table = root-table
@filename = options.filename || null
# The import-target-macro-tables argument is for the situation when a macro
# returns another macro. In such a case, the returned macro should be
# added to the tables specified (the scope the macro that created it was
# in, as well as the scope of other statements during that compile) not to
# the table representing the scope of the outer macro's contents.
#
# If that's confusing, take a few deep breaths and read it again. Welcome
# to the blissful land of Lisp, where everything is recursive somehow.
@import-target-macro-tables = options.import-target-macro-tables
atom : (value) -> { type : \atom, value : value.to-string! }
string : (value) -> { type : \string, value }
list : (...values) -> { type : \list, values }
compile : ~> # compile to estree
compile this, it
compile-to-quote : ~> # compile to estree that produces this AST node
compile.to-self-producer this, it
compile-many : ~> it |> concat-map @compile |> (.filter (?))
compile-to-js : -> es-generate it
evaluate : ~>
ast = it |> @compile
js = ast |> @compile-to-js
if ast.type is \ObjectExpression
js |> (-> "(#it)") |> eval
else
js |> eval
#
# Because object expressions generated by escodegen (e.g. "{ a: 1 }") are
# liable to be read by `eval` as block statements ("{}") containing a
# labelled ("a:") literal ("1"), we guard against that here with the
# [classic trick][1] of first wrapping object expressions in parentheses
# before evaluating them.
#
# [1]: http://stackoverflow.com/questions/3360356
#
# - - -
#
# With everything else, we just straight-up eval. This needs to stay the
# default case because we also want to be able to evaluate statements.
#
# Always wrapping everything in parentheses before eval would only work
# for expressions. For statements, it's nonsensical. For example,
#
# (if (a) {})
#
# gives "SyntaxError: Unexpected token if".
#
derive : ~>
# Create a derived environment with this one as its parent. This
# implements macro scope; macros defined in the new environment aren't
# visible in the outer one.
env @macro-table, { @import-target-macro-tables, @filename }
derive-flattened : ~>
# This method creates a derived environment with its macro table
# "flattened" to keep a safe local copy of the current compilation
# environment. This preserves lexical scoping.
# To expand a bit more on that: This fixes situations where a macro, which
# the now-defined macro uses, is redefined later. The redefinition should
# not affect this macro's behaviour, so we have to hold on to a copy of the
# environment as it was when we defined this.
flattened-macro-table = flatten-macro-table @macro-table
# Use the previously stored macro scope
table-to-read-from = flattened-macro-table
# Import macros both into the outer scope...
tables-to-import-into =
if @import-target-macro-tables then clone-array that
else [ @macro-table ]
# ... and the current compilation's scope
tables-to-import-into
..push flattened-macro-table
env do
table-to-read-from
{
import-target-macro-tables : tables-to-import-into
@filename
}
derive-root : ~>
root-table = find-root @macro-table
import-targets = (@import-target-macro-tables || [ @macro-table ])
env do
root-table
{ import-target-macro-tables : import-targets }
find-macro : (name) ~> find-macro @macro-table, name
import-macro : (name, func) ~>
# The func argument can also be null in order to mask the macro of the
# given name in this scope. This works because the `find-macro` operation
# will quit when it finds a null in the macro table, returning the result
# that such a macro was not found.
# If the import target macro table is available, import the macro to that.
# Otherwise, import it to the usual table.
if @import-target-macro-tables
that .for-each (.parent.contents[name] = func)
else
@macro-table.parent.contents[name] = func
module.exports = env