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

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