OILS / builtin / hay_ysh.py View on Github | oils.pub

433 lines, 221 significant
1from __future__ import print_function
2
3from _devbuild.gen.option_asdl import option_i
4from _devbuild.gen.runtime_asdl import (scope_e, HayNode)
5from _devbuild.gen.syntax_asdl import loc
6from _devbuild.gen.value_asdl import (value, value_e, value_t, LiteralBlock,
7 cmd_frag, cmd_frag_e)
8
9from asdl import format as fmt
10from core import alloc
11from core.error import e_usage, e_die
12from core import num
13from core import state
14from display import ui
15from core import vm
16from frontend import args
17from frontend import consts
18from frontend import location
19from frontend import typed_args
20from mycpp import mylib
21from mycpp.mylib import tagswitch, NewDict, log
22
23from typing import List, Dict, Optional, Any, cast, TYPE_CHECKING
24if TYPE_CHECKING:
25 from _devbuild.gen.runtime_asdl import cmd_value
26 from osh.cmd_eval import CommandEvaluator
27
28_ = log
29
30_HAY_ACTION_ERROR = "builtin expects 'define', 'reset' or 'pp'"
31
32
33class ctx_HayNode(object):
34 """Haynode builtin makes new names in the tree visible."""
35
36 def __init__(self, hay_state, hay_name):
37 # type: (HayState, Optional[str]) -> None
38 #log('pairs %s', pairs)
39 self.hay_state = hay_state
40 self.hay_state.Push(hay_name)
41
42 def __enter__(self):
43 # type: () -> None
44 return
45
46 def __exit__(self, type, value, traceback):
47 # type: (Any, Any, Any) -> None
48 self.hay_state.Pop()
49
50
51class ctx_HayEval(object):
52 """
53 - Turn on shopt ysh:all and _running_hay
54 - Disallow recursive 'hay eval'
55 - Ensure result is isolated for 'hay eval :result'
56
57 More leakage:
58
59 External:
60 - execute programs (ext_prog)
61 - redirect
62 - pipelines, subshell, & etc?
63 - do you have to put _running_hay() checks everywhere?
64
65 Internal:
66
67 - state.Mem()
68 - should we at least PushTemp()?
69 - But then they can do setglobal
70 - Option state
71
72 - Disallow all builtins except echo/write/printf?
73 - maybe could do that at the top level
74 - source builtin, read builtin
75 - cd / pushd / popd
76 - trap -- hm yeah this one is bad
77
78 - procs? Not strictly necessary
79 - you should be able to define them, but not call the user ...
80
81 """
82
83 def __init__(self, hay_state, mutable_opts, mem):
84 # type: (HayState, state.MutableOpts, state.Mem) -> None
85 self.hay_state = hay_state
86 self.mutable_opts = mutable_opts
87 self.mem = mem
88
89 if mutable_opts.Get(option_i._running_hay):
90 # This blames the right 'hay' location
91 e_die("Recursive 'hay eval' not allowed")
92
93 for opt_num in consts.YSH_ALL:
94 mutable_opts.Push(opt_num, True)
95 mutable_opts.Push(option_i._running_hay, True)
96
97 self.hay_state.PushEval()
98 self.mem.PushTemp()
99
100 def __enter__(self):
101 # type: () -> None
102 return
103
104 def __exit__(self, type, value, traceback):
105 # type: (Any, Any, Any) -> None
106
107 self.mem.PopTemp()
108 self.hay_state.PopEval()
109
110 self.mutable_opts.Pop(option_i._running_hay)
111 for opt_num in consts.YSH_ALL:
112 self.mutable_opts.Pop(opt_num)
113
114
115class HayState(object):
116 """State for DSLs."""
117
118 def __init__(self):
119 # type: () -> None
120 ch = NewDict() # type: Dict[str, HayNode]
121 self.root_defs = HayNode(ch)
122 self.cur_defs = self.root_defs # Same as ClearDefs()
123 self.def_stack = [self.root_defs]
124
125 node = self._MakeOutputNode()
126 self.result_stack = [node] # type: List[Dict[str, value_t]]
127 self.output = None # type: Dict[str, value_t]
128
129 def _MakeOutputNode(self):
130 # type: () -> Dict[str, value_t]
131 d = NewDict() # type: Dict[str, value_t]
132 d['source'] = value.Null
133 d['children'] = value.List([])
134 return d
135
136 def PushEval(self):
137 # type: () -> None
138
139 # remove previous results
140 node = self._MakeOutputNode()
141 self.result_stack = [node]
142
143 self.output = None # remove last result
144
145 def PopEval(self):
146 # type: () -> None
147
148 # Save the result
149 self.output = self.result_stack[0]
150
151 # Clear results
152 node = self._MakeOutputNode()
153 self.result_stack = [node]
154
155 def AppendResult(self, d):
156 # type: (Dict[str, value_t]) -> None
157 """Called by haynode builtin."""
158 UP_children = self.result_stack[-1]['children']
159 assert UP_children.tag() == value_e.List, UP_children
160 children = cast(value.List, UP_children)
161 children.items.append(value.Dict(d))
162
163 def Result(self):
164 # type: () -> Dict[str, value_t]
165 """Called by hay eval and eval_hay()"""
166 return self.output
167
168 def HayRegister(self):
169 # type: () -> Dict[str, value_t]
170 """Called by _hay() function."""
171 return self.result_stack[0]
172
173 def Resolve(self, first_word):
174 # type: (str) -> bool
175 return first_word in self.cur_defs.children
176
177 def DefinePath(self, path):
178 # type: (List[str]) -> None
179 """Fill a tree from the given path."""
180 current = self.root_defs
181 for name in path:
182 if name not in current.children:
183 ch = NewDict() # type: Dict[str, HayNode]
184 current.children[name] = HayNode(ch)
185 current = current.children[name]
186
187 def Reset(self):
188 # type: () -> None
189
190 # reset definitions
191 ch = NewDict() # type: Dict[str, HayNode]
192 self.root_defs = HayNode(ch)
193 self.cur_defs = self.root_defs
194
195 # reset output
196 self.PopEval()
197
198 def Push(self, hay_name):
199 # type: (Optional[str]) -> None
200 """
201 Package cppunit {
202 } # pushes a namespace
203
204 haynode package cppunit {
205 } # just assumes every TYPE 'package' is valid.
206 """
207 top = self.result_stack[-1]
208 # TODO: Store this more efficiently? See osh/builtin_pure.py
209 children = cast(value.List, top['children'])
210 last_child = cast(value.Dict, children.items[-1])
211 self.result_stack.append(last_child.d)
212
213 #log('> PUSH')
214 if hay_name is None:
215 self.def_stack.append(self.cur_defs) # no-op
216 else:
217 # Caller should ensure this
218 assert hay_name in self.cur_defs.children, hay_name
219
220 self.cur_defs = self.cur_defs.children[hay_name]
221 self.def_stack.append(self.cur_defs)
222
223 def Pop(self):
224 # type: () -> None
225 self.def_stack.pop()
226 self.cur_defs = self.def_stack[-1]
227
228 self.result_stack.pop()
229
230
231class Hay(vm._Builtin):
232 """hay builtin
233
234 hay define -- package user
235 hay define -- user/foo user/bar # second level
236 hay pp
237 hay reset
238 """
239
240 def __init__(self, hay_state, mutable_opts, mem, cmd_ev):
241 # type: (HayState, state.MutableOpts, state.Mem, CommandEvaluator) -> None
242 self.hay_state = hay_state
243 self.mutable_opts = mutable_opts
244 self.mem = mem
245 self.cmd_ev = cmd_ev # To run blocks
246
247 def Run(self, cmd_val):
248 # type: (cmd_value.Argv) -> int
249 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
250 arg_r.Next() # skip 'hay'
251
252 action, action_loc = arg_r.Peek2()
253 if action is None:
254 e_usage(_HAY_ACTION_ERROR, action_loc)
255 arg_r.Next()
256
257 if action == 'define':
258 # TODO: accept --
259 #arg, arg_r = flag_spec.ParseCmdVal('hay-define', cmd_val)
260
261 # arg = args.Parse(JSON_WRITE_SPEC, arg_r)
262 first, _ = arg_r.Peek2()
263 if first is None:
264 e_usage('define expected a name', action_loc)
265
266 names, name_locs = arg_r.Rest2()
267 for i, name in enumerate(names):
268 path = name.split('/')
269 for p in path:
270 if len(p) == 0:
271 e_usage(
272 "got invalid path %r. Parts can't be empty." %
273 name, name_locs[i])
274 self.hay_state.DefinePath(path)
275
276 elif action == 'eval':
277 # hay eval :myvar { ... }
278 #
279 # - turn on ysh:all
280 # - set _running_hay -- so that hay "first words" are visible
281 # - then set the variable name to the result
282
283 var_name, _ = arg_r.ReadRequired2("expected variable name")
284 if var_name.startswith(':'):
285 var_name = var_name[1:]
286 # TODO: This could be fatal?
287
288 cmd = typed_args.RequiredBlockAsFrag(cmd_val)
289
290 with ctx_HayEval(self.hay_state, self.mutable_opts, self.mem):
291 # Note: we want all haynode invocations in the block to appear as
292 # our 'children', recursively
293 unused = self.cmd_ev.EvalCommandFrag(cmd)
294
295 result = self.hay_state.Result()
296
297 val = value.Dict(result)
298 self.mem.SetNamed(location.LName(var_name), val, scope_e.LocalOnly)
299
300 elif action == 'reset':
301 self.hay_state.Reset()
302
303 elif action == 'pp':
304 h = self.hay_state.root_defs.PrettyTree(False)
305 f = mylib.Stdout()
306 fmt.HNodePrettyPrint(h, f)
307 f.write('\n')
308
309 else:
310 e_usage(_HAY_ACTION_ERROR, action_loc)
311
312 return 0
313
314
315class HayNode_(vm._Builtin):
316 """The FIXED builtin that is run after 'hay define'.
317
318 It evaluates a SUBTREE
319
320 Example:
321
322 package cppunit {
323 version = '1.0'
324 user bob
325 }
326
327 is short for
328
329 haynode package cppunit {
330 version = '1.0'
331 haynode user bob
332 }
333 """
334
335 def __init__(self, hay_state, mem, cmd_ev):
336 # type: (HayState, state.Mem, CommandEvaluator) -> None
337 self.hay_state = hay_state
338 self.mem = mem # isolation with mem.PushTemp
339 self.cmd_ev = cmd_ev # To run blocks
340 self.arena = cmd_ev.arena # To extract code strings
341
342 def Run(self, cmd_val):
343 # type: (cmd_value.Argv) -> int
344
345 arg_r = args.Reader(cmd_val.argv, locs=cmd_val.arg_locs)
346
347 hay_name, arg0_loc = arg_r.Peek2()
348 if hay_name == 'haynode': # haynode package glib { ... }
349 arg_r.Next()
350 hay_name = None # don't validate
351
352 rd = typed_args.ReaderForProc(cmd_val)
353 cmd = rd.OptionalCommand()
354 rd.Done()
355
356 lit_block = None # type: Optional[LiteralBlock]
357 if cmd:
358 frag = cmd.frag
359 with tagswitch(frag) as case:
360 if case(cmd_frag_e.LiteralBlock):
361 lit_block = cast(LiteralBlock, frag)
362 elif case(cmd_frag_e.Expr):
363 c = cast(cmd_frag.Expr, frag).c
364 # This can happen with Node (; ; ^(echo hi))
365 # The problem is that it doesn't have "backing lines",
366 # which the Hay API uses.
367 e_die("Hay expected block literal, like { echo x }",
368 loc.Command(c))
369 else:
370 raise AssertionError()
371
372 # Should we call hay_state.AddChild() so it can be mutated?
373 result = NewDict() # type: Dict[str, value_t]
374
375 node_type, _ = arg_r.Peek2()
376 result['type'] = value.Str(node_type)
377
378 arg_r.Next()
379 arguments = arg_r.Rest()
380
381 # package { ... } is not valid
382 if len(arguments) == 0 and lit_block is None:
383 e_usage('expected at least 1 arg, or a literal block { }',
384 arg0_loc)
385
386 items = [value.Str(s) for s in arguments] # type: List[value_t]
387 result['args'] = value.List(items)
388
389 if node_type.isupper(): # TASK build { ... }
390 if lit_block is None:
391 e_usage('command node requires a literal block argument',
392 loc.Missing)
393
394 # We can only extract code if the block arg is literal like package
395 # foo { ... }, not if it's like package foo (myblock)
396
397 brace_group = lit_block.brace_group
398 # BraceGroup has location for {
399 line = brace_group.left.line
400
401 # for the user to pass back to --location-str
402 result['location_str'] = value.Str(ui.GetLineSourceString(line))
403 result['location_start_line'] = num.ToBig(line.line_num)
404
405 #log('LINES %s', lit_block.lines)
406 # Between { and }
407 code_str = alloc.SnipCodeBlock(brace_group.left, brace_group.right,
408 lit_block.lines)
409
410 result['code_str'] = value.Str(code_str)
411
412 # Append after validation
413 self.hay_state.AppendResult(result)
414
415 else:
416 # Must be done before EvalCommand
417 self.hay_state.AppendResult(result)
418
419 if lit_block: # 'package foo' is OK
420 result['children'] = value.List([])
421
422 # Note: this is based on evalToDict()
423 unbound_frag = typed_args.GetCommandFrag(cmd)
424 bindings = NewDict() # type: Dict[str, value_t]
425 with ctx_HayNode(self.hay_state, hay_name):
426 with state.ctx_EnclosedFrame(self.mem, cmd.captured_frame,
427 cmd.module_frame, bindings):
428 unused_status = self.cmd_ev.EvalCommandFrag(
429 unbound_frag)
430
431 result['attrs'] = value.Dict(bindings)
432
433 return 0