fix weird interaction between let* and fn* and atoms
[jackhill/mal.git] / jq / env.jq
1 include "utils";
2
3 def childEnv(binds; exprs):
4 {
5 parent: .,
6 fallback: null,
7 dirty_atoms: .dirty_atoms,
8 environment: [binds, exprs] | transpose | (
9 . as $dot | reduce .[] as $item (
10 { value: [], seen: false, name: null, idx: 0 };
11 if $item[1] != null then
12 if .seen then
13 {
14 value: (.value[1:-1] + (.value|last[1].value += [$item[1]])),
15 seen: true,
16 name: .name
17 }
18 else
19 if $item[0] == "&" then
20 $dot[.idx+1][0] as $name | {
21 value: (.value + [[$name, {kind:"list", value: [$item[1]]}]]),
22 seen: true,
23 name: $name
24 }
25 else
26 {
27 value: (.value + [$item]),
28 seen: false,
29 name: null
30 }
31 end
32 end | (.idx |= .idx + 1)
33 else
34 if $item[0] == "&" then
35 $dot[.idx+1][0] as $name | {
36 value: (.value + [[$name, {kind:"list", value: []}]]),
37 seen: true,
38 name: $name
39 }
40 else . end
41 end
42 )
43 ) | .value | map({(.[0]): .[1]}) | add
44 };
45
46 def pureChildEnv:
47 {
48 parent: .,
49 environment: {},
50 fallback: null,
51 dirty_atoms: .dirty_atoms
52 };
53
54 def rootEnv:
55 {
56 parent: null,
57 fallback: null,
58 environment: {},
59 dirty_atoms: []
60 };
61
62 def inform_function(name):
63 (.names += [name]) | (.names |= unique);
64
65 def inform_function_multi(names):
66 . as $dot | reduce names[] as $name(
67 $dot;
68 inform_function($name)
69 );
70
71 def env_multiset(keys; value):
72 (if value.kind == "function" then # multiset not allowed on atoms
73 value | inform_function_multi(keys)
74 else
75 value
76 end) as $value | {
77 parent: .parent,
78 environment: (
79 .environment + (reduce keys[] as $key(.environment; .[$key] |= value))
80 ),
81 fallback: .fallback,
82 dirty_atoms: .dirty_atoms
83 };
84
85 def env_multiset(env; keys; value):
86 env | env_multiset(keys; value);
87
88 def env_set($key; $value):
89 (if $value.kind == "function" or $value.kind == "atom" then
90 # inform the function/atom of its names
91 ($value |
92 if $value.kind == "atom" then
93 # check if the one we have is newer
94 env_req(env; key) as $ours |
95 if $ours.last_modified > $value.last_modified then
96 $ours
97 else
98 # update modification timestamp
99 $value | .last_modified |= now
100 end
101 else
102 .
103 end) | inform_function($key)
104 else
105 $value
106 end) as $value | {
107 parent: .parent,
108 environment: (.environment + (.environment | .[$key] |= $value)), # merge together, as .environment[key] |= value does not work
109 fallback: .fallback,
110 dirty_atoms: .dirty_atoms
111 };
112
113 def env_dump_keys:
114 def _dump1:
115 .environment // {} | keys;
116 if . == null then [] else
117 if .parent == null then
118 (
119 _dump1 +
120 (.fallback | env_dump_keys)
121 )
122 else
123 (
124 _dump1 +
125 (.parent | env_dump_keys) +
126 (.fallback | env_dump_keys)
127 )
128 end | unique
129 end;
130
131 def env_find(env):
132 if env.environment[.] == null then
133 if env.parent then
134 env_find(env.parent) // if env.fallback then env_find(env.fallback) else null end
135 else
136 null
137 end
138 else
139 env
140 end;
141
142 def env_get(env):
143 . as $key | $key | env_find(env).environment[$key] as $value |
144 if $value == null then
145 jqmal_error("'\($key)' not found")
146 else
147 if $value.kind == "atom" then
148 $value.identity as $id |
149 $key | env_find(env.parent).environment[$key] as $possibly_newer |
150 if $possibly_newer.identity == $id and $possibly_newer.last_modified > $value.last_modified then
151 $possibly_newer
152 else
153 $value
154 end
155 else
156 $value
157 end
158 end;
159
160 def env_get(env; key):
161 key | env_get(env);
162
163 def env_req(env; key):
164 key as $key | key | env_find(env).environment[$key] as $value |
165 if $value == null then
166 null
167 else
168 if $value.kind == "atom" then
169 $value.identity as $id |
170 $key | env_find(env.parent).environment[$key] as $possibly_newer |
171 if $possibly_newer.identity == $id and $possibly_newer.last_modified > $value.last_modified then
172 $possibly_newer
173 else
174 $value
175 end
176 else
177 $value
178 end
179 end;
180
181 def env_set(env; $key; $value):
182 (if $value.kind == "function" or $value.kind == "atom" then
183 # inform the function/atom of its names
184 $value | (.names += [$key]) | (.names |= unique) |
185 if $value.kind == "atom" then
186 # check if the one we have is newer
187 env_req(env; $key) as $ours |
188 if $ours.last_modified > $value.last_modified then
189 $ours
190 else
191 # update modification timestamp
192 $value | .last_modified |= now
193 end
194 else
195 .
196 end
197 else
198 $value
199 end) as $value | {
200 parent: env.parent,
201 environment: ((env.environment // jqmal_error("Environment empty in \(env | keys)")) + (env.environment | .[$key] |= $value)), # merge together, as env.environment[key] |= value does not work
202 fallback: env.fallback,
203 dirty_atoms: env.dirty_atoms
204 };
205
206 def env_setfallback(env; fallback):
207 {
208 parent: env.parent,
209 fallback: fallback,
210 environment: env.environment,
211 dirty_atoms: env.dirty_atoms
212 };
213
214 def addEnv(env):
215 {
216 expr: .,
217 env: env
218 };
219
220 def addToEnv(env; name; expr):
221 {
222 expr: expr,
223 env: env_set(env; name; expr)
224 };
225
226
227 def wrapEnv:
228 {
229 replEnv: .,
230 currentEnv: .,
231 isReplEnv: true
232 };
233
234 def wrapEnv(replEnv):
235 {
236 replEnv: replEnv,
237 currentEnv: .,
238 isReplEnv: (replEnv == .) # should we allow separate copies?
239 };
240
241 def unwrapReplEnv:
242 .replEnv;
243
244 def unwrapCurrentEnv:
245 .currentEnv;
246
247 def env_set6(env; key; value):
248 if env.isReplEnv then
249 env_set(env.currentEnv; key; value) | wrapEnv
250 else
251 env_set(env.currentEnv; key; value) | wrapEnv(env.replEnv)
252 end;
253
254 def env_set_(env; key; value):
255 if env.currentEnv != null then
256 env_set6(env; key; value)
257 else
258 env_set(env; key; value)
259 end;
260
261 def addToEnv6(envexp; name):
262 envexp.expr as $value
263 | envexp.env as $rawEnv
264 | (if $rawEnv.isReplEnv then
265 env_set_($rawEnv.currentEnv; name; $value) | wrapEnv
266 else
267 env_set_($rawEnv.currentEnv; name; $value) | wrapEnv($rawEnv.replEnv)
268 end) as $newEnv
269 | {
270 expr: $value,
271 env: $newEnv
272 };
273
274 def addToEnv(envexp; name):
275 if envexp.env.replEnv != null then
276 addToEnv6(envexp; name)
277 else {
278 expr: envexp.expr,
279 env: env_set_(envexp.env; name; envexp.expr)
280 } end;
281
282 def _env_remove_references(refs):
283 if . != null then
284 {
285 environment: (.environment | to_entries | map(select(.key as $key | refs | contains([$key]) | not)) | from_entries),
286 parent: (.parent | _env_remove_references(refs)),
287 fallback: (.fallback | _env_remove_references(refs)),
288 dirty_atoms: (.dirty_atoms | map(select(. as $dot | refs | contains([$dot]) | not)))
289 }
290 else . end;
291
292 def env_remove_references(refs):
293 . as $env
294 | if has("replEnv") then
295 .currentEnv |= _env_remove_references(refs)
296 else
297 _env_remove_references(refs)
298 end;
299
300 # for step2
301 def lookup(env):
302 env.environment[.] //
303 if env.parent then
304 lookup(env.parent)
305 else
306 jqmal_error("'\(.)' not found")
307 end;