Skip to content

Commit 59479a5

Browse files
author
Andy C
committed
[builtin/ysh] Add pure functions eval() and evalExpr()
This was straightforward! It fell out of the clean implementation.
1 parent a626773 commit 59479a5

11 files changed

+181
-78
lines changed

builtin/method_io.py

+21-9
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,28 @@ class EvalExpr(vm._Callable):
4444
"""io->evalExpr(ex) evaluates an expression
4545
4646
Notes compared with io->eval(cmd):
47-
- there is no evalToDict variant - doesn't make sense
47+
- there is no to_dict=true variant - doesn't make sense
4848
- Does it need in_captured_frame=true?
4949
- That is for "inline procs" like cd, but doesn't seem to be necessary
5050
for expressions. Unless we had mutations in expressions.
5151
"""
5252

53-
def __init__(self, expr_ev):
54-
# type: (expr_eval.ExprEvaluator) -> None
53+
def __init__(
54+
self,
55+
expr_ev, # type: expr_eval.ExprEvaluator
56+
pure_ex, # type: Optional[vm._Executor]
57+
cmd_ev, # type: Optional[cmd_eval.CommandEvaluator]
58+
):
59+
# type: (...) -> None
5560
self.expr_ev = expr_ev
61+
self.pure_ex = pure_ex
62+
self.cmd_ev = cmd_ev
5663
self.mem = expr_ev.mem
5764

5865
def Call(self, rd):
5966
# type: (typed_args.Reader) -> value_t
60-
unused_self = rd.PosObj()
67+
if self.pure_ex is None:
68+
unused_self = rd.PosObj()
6169
lazy = rd.PosExpr()
6270

6371
dollar0 = rd.NamedStr("dollar0", None)
@@ -74,7 +82,8 @@ def Call(self, rd):
7482
with state.ctx_EnclosedFrame(self.mem, lazy.captured_frame,
7583
lazy.module_frame, None):
7684
with state.ctx_Eval(self.mem, dollar0, pos_args, vars_):
77-
result = self.expr_ev.EvalExpr(lazy.e, blame_tok)
85+
with vm.ctx_MaybePure(self.pure_ex, self.cmd_ev):
86+
result = self.expr_ev.EvalExpr(lazy.e, blame_tok)
7887

7988
return result
8089

@@ -139,15 +148,17 @@ class Eval(vm._Callable):
139148
The CALLER must handle errors.
140149
"""
141150

142-
def __init__(self, mem, cmd_ev, which):
143-
# type: (state.Mem, cmd_eval.CommandEvaluator, int) -> None
151+
def __init__(self, mem, cmd_ev, pure_ex, which):
152+
# type: (state.Mem, cmd_eval.CommandEvaluator, Optional[vm._Executor], int) -> None
144153
self.mem = mem
145154
self.cmd_ev = cmd_ev
155+
self.pure_ex = pure_ex
146156
self.which = which
147157

148158
def Call(self, rd):
149159
# type: (typed_args.Reader) -> value_t
150-
unused = rd.PosValue()
160+
if self.pure_ex is None:
161+
unused = rd.PosValue()
151162
bound = rd.PosCommand()
152163

153164
dollar0 = rd.NamedStr("dollar0", None)
@@ -180,7 +191,8 @@ def Call(self, rd):
180191
inside=in_captured_frame):
181192
# _PrintFrame('[new]', self.cmd_ev.mem.var_stack[-1])
182193
with state.ctx_Eval(self.mem, dollar0, pos_args, vars_):
183-
unused_status = self.cmd_ev.EvalCommandFrag(frag)
194+
with vm.ctx_MaybePure(self.pure_ex, self.cmd_ev):
195+
unused_status = self.cmd_ev.EvalCommandFrag(frag)
184196

185197
if bindings is not None:
186198
return value.Dict(bindings)

core/executor.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,7 @@ def __init__(
222222
tracer, # type: dev.Tracer
223223
errfmt # type: ui.ErrorFormatter
224224
):
225-
pass
226-
227-
def CheckCircularDeps(self):
228-
# type: () -> None
229-
pass
225+
vm._Executor.__init__(self)
230226

231227
def RunBuiltin(self, builtin_id, cmd_val):
232228
# type: (int, cmd_value.Argv) -> int
@@ -245,7 +241,8 @@ def RunBackgroundJob(self, node):
245241

246242
def RunPipeline(self, node, status_out):
247243
# type: (command.Pipeline, CommandStatus) -> None
248-
e_die("Pipelines aren't allowed in pure mode (OILS-ERR-204)", loc.Command(node))
244+
e_die("Pipelines aren't allowed in pure mode (OILS-ERR-204)",
245+
loc.Command(node))
249246

250247
def RunSubshell(self, node):
251248
# type: (command_t) -> int
@@ -260,7 +257,8 @@ def CaptureStdout(self, node):
260257

261258
def RunCommandSub(self, cs_part):
262259
# type: (CommandSub) -> str
263-
e_die("Command subs aren't allowed in pure mode (OILS-ERR-204)", loc.WordPart(cs_part))
260+
e_die("Command subs aren't allowed in pure mode (OILS-ERR-204)",
261+
loc.WordPart(cs_part))
264262
return ''
265263

266264
def RunProcessSub(self, cs_part):
@@ -337,10 +335,6 @@ def __init__(
337335
# group, we only need one pointer here, not some collection.
338336
self.fg_pipeline = None # type: Optional[process.Pipeline]
339337

340-
def CheckCircularDeps(self):
341-
# type: () -> None
342-
assert self.cmd_ev is not None
343-
344338
def _MakeProcess(self, node, inherit_errexit, inherit_errtrace):
345339
# type: (command_t, bool, bool) -> process.Process
346340
"""Assume we will run the node in another process.

core/shell.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -524,16 +524,17 @@ def Main(
524524

525525
# The M/ prefix means it's io->eval()
526526
io_methods['M/eval'] = value.BuiltinFunc(
527-
method_io.Eval(mem, cmd_ev, method_io.EVAL_NULL))
528-
io_methods['M/evalExpr'] = value.BuiltinFunc(method_io.EvalExpr(expr_ev))
527+
method_io.Eval(mem, cmd_ev, None, method_io.EVAL_NULL))
528+
io_methods['M/evalExpr'] = value.BuiltinFunc(
529+
method_io.EvalExpr(expr_ev, None, None))
529530

530531
# Identical to command sub
531532
io_methods['captureStdout'] = value.BuiltinFunc(
532533
method_io.CaptureStdout(mem, shell_ex))
533534

534535
# TODO: remove these 2 deprecated methods
535536
io_methods['M/evalToDict'] = value.BuiltinFunc(
536-
method_io.Eval(mem, cmd_ev, method_io.EVAL_DICT))
537+
method_io.Eval(mem, cmd_ev, None, method_io.EVAL_DICT))
537538
io_methods['M/evalInFrame'] = value.BuiltinFunc(
538539
method_io.EvalInFrame(mem, cmd_ev))
539540

@@ -600,7 +601,7 @@ def Main(
600601

601602
# Wire up circular dependencies.
602603
vm.InitCircularDeps(arith_ev, bool_ev, expr_ev, word_ev, cmd_ev, shell_ex,
603-
prompt_ev, io_obj, tracer)
604+
pure_ex, prompt_ev, io_obj, tracer)
604605

605606
unsafe_arith = sh_expr_eval.UnsafeArith(mem, exec_opts, mutable_opts,
606607
parse_ctx, arith_ev, errfmt)
@@ -871,6 +872,12 @@ def Main(
871872
# Initialize Built-in Funcs
872873
#
873874

875+
# Pure functions
876+
_AddBuiltinFunc(mem, 'eval',
877+
method_io.Eval(mem, cmd_ev, pure_ex, method_io.EVAL_NULL))
878+
_AddBuiltinFunc(mem, 'evalExpr',
879+
method_io.EvalExpr(expr_ev, pure_ex, cmd_ev))
880+
874881
parse_hay = func_hay.ParseHay(fd_state, parse_ctx, mem, errfmt)
875882
eval_hay = func_hay.EvalHay(hay_state, mutable_opts, mem, cmd_ev)
876883
hay_func = func_hay.HayFunc(hay_state)
@@ -967,7 +974,7 @@ def Main(
967974

968975
for path, is_pure in attrs.eval_flags:
969976
ex = pure_ex if is_pure else None
970-
with vm.ctx_MaybePure(ex, cmd_ev, word_ev, expr_ev):
977+
with vm.ctx_MaybePure(ex, cmd_ev):
971978
try:
972979
ok, status = main_loop.EvalFile(path, fd_state, parse_ctx,
973980
cmd_ev, lang)

core/test_lib.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -290,13 +290,15 @@ def InitCommandEvaluator(parse_ctx=None,
290290
hay_state, builtins, search_path,
291291
ext_prog, waiter, tracer, job_control,
292292
job_list, fd_state, trap_state, errfmt)
293+
pure_ex = executor.PureExecutor(mem, exec_opts, mutable_opts, procs,
294+
hay_state, builtins, tracer, errfmt)
293295

294296
assert cmd_ev.mutable_opts is not None, cmd_ev
295297
prompt_ev = prompt.Evaluator('osh', '0.0.0', parse_ctx, mem)
296298

297299
global_io = Obj(None, None)
298300
vm.InitCircularDeps(arith_ev, bool_ev, expr_ev, word_ev, cmd_ev, shell_ex,
299-
prompt_ev, global_io, tracer)
301+
pure_ex, prompt_ev, global_io, tracer)
300302

301303
try:
302304
from _devbuild.gen.help_meta import TOPICS

core/vm.py

+15-10
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def InitCircularDeps(
122122
word_ev, # type: word_eval.NormalWordEvaluator
123123
cmd_ev, # type: cmd_eval.CommandEvaluator
124124
shell_ex, # type: _Executor
125+
pure_ex, # type: _Executor
125126
prompt_ev, # type: prompt.Evaluator
126127
global_io, # type: Obj
127128
tracer, # type: dev.Tracer
@@ -149,6 +150,7 @@ def InitCircularDeps(
149150
cmd_ev.tracer = tracer
150151

151152
shell_ex.cmd_ev = cmd_ev
153+
pure_ex.cmd_ev = cmd_ev
152154

153155
prompt_ev.word_ev = word_ev
154156
prompt_ev.expr_ev = expr_ev
@@ -163,6 +165,7 @@ def InitCircularDeps(
163165
word_ev.CheckCircularDeps()
164166
cmd_ev.CheckCircularDeps()
165167
shell_ex.CheckCircularDeps()
168+
pure_ex.CheckCircularDeps()
166169
prompt_ev.CheckCircularDeps()
167170
tracer.CheckCircularDeps()
168171

@@ -175,7 +178,7 @@ def __init__(self):
175178

176179
def CheckCircularDeps(self):
177180
# type: () -> None
178-
pass
181+
assert self.cmd_ev is not None
179182

180183
def RunBuiltin(self, builtin_id, cmd_val):
181184
# type: (int, cmd_value.Argv) -> int
@@ -285,28 +288,29 @@ class ctx_MaybePure(object):
285288
"""
286289

287290
def __init__(
288-
self,
289-
pure_ex, # type: Optional[_Executor]
290-
cmd_ev, # type: cmd_eval.CommandEvaluator
291-
word_ev, # type: word_eval.NormalWordEvaluator
292-
expr_ev # type: expr_eval.ExprEvaluator
291+
self,
292+
pure_ex, # type: Optional[_Executor]
293+
cmd_ev, # type: cmd_eval.CommandEvaluator
293294
):
294295
# type: (...) -> None
295296
self.pure_ex = pure_ex
296297
if not pure_ex:
297298
return # do nothing
298299

300+
word_ev = cmd_ev.word_ev
301+
expr_ev = cmd_ev.expr_ev
302+
303+
# Save the Shell Executor
299304
self.saved = cmd_ev.shell_ex
300305
assert self.saved is word_ev.shell_ex
301306
assert self.saved is expr_ev.shell_ex
302307

308+
# Patch evaluators to use the Pure Executor
303309
cmd_ev.shell_ex = pure_ex
304310
word_ev.shell_ex = pure_ex
305311
expr_ev.shell_ex = pure_ex
306312

307313
self.cmd_ev = cmd_ev
308-
self.word_ev = word_ev
309-
self.expr_ev = expr_ev
310314

311315
def __enter__(self):
312316
# type: () -> None
@@ -317,9 +321,10 @@ def __exit__(self, type, value, traceback):
317321
if not self.pure_ex:
318322
return # do nothing
319323

324+
# Unpatch the evaluators
320325
self.cmd_ev.shell_ex = self.saved
321-
self.word_ev.shell_ex = self.saved
322-
self.expr_ev.shell_ex = self.saved
326+
self.cmd_ev.word_ev.shell_ex = self.saved
327+
self.cmd_ev.expr_ev.shell_ex = self.saved
323328

324329

325330
class ctx_Redirect(object):

doc/ref/chap-builtin-func.md

+25
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,31 @@ Like `Match => end()`, but accesses the global match created by `~`:
373373
echo $[_end(0)] # => 5
374374
}
375375

376+
## Reflection
377+
378+
### func/eval()
379+
380+
This function is like [`io->eval()`][io/eval], but it disallows I/O.
381+
382+
Example:
383+
384+
var cmd = ^(const x = 42)
385+
var d = eval(cmd, to_dict=true) # {x: 42}
386+
387+
[io/eval]: chap-type-method.html#io/eval
388+
389+
### func/evalExpr()
390+
391+
This function is like [`io->evalExpr()`][io/evalExpr], but it disallows I/O.
392+
393+
Example:
394+
395+
var x = 42
396+
var expr = ^[x + 1]
397+
var val = evalExpr(expr) # 43
398+
399+
[io/evalExpr]: chap-type-method.html#io/evalExpr
400+
376401
## Introspect
377402

378403
### `shvarGet()`

doc/ref/chap-index.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,23 @@ The name `clear` can refer to:
4343

4444
The name `eval` can refer to:
4545

46-
- The [eval builtin][cmd/eval]
47-
- The [io method eval][io/eval]
46+
- The [eval builtin command][cmd/eval]
47+
- The [io method eval()][io/eval]
48+
- The [pure function eval()][func/eval]
4849

4950
[cmd/eval]: chap-builtin-cmd.html#cmd/eval
5051
[io/eval]: chap-type-method.html#io/eval
52+
[func/eval]: chap-builtin-func.html#func/eval
53+
54+
### evalExpr
55+
56+
The name `eval` can refer to:
57+
58+
- The [io method evalExpr()][io/evalExpr]
59+
- The [pure function evalExpr()][func/evalExpr]
60+
61+
[io/evalExpr]: chap-type-method.html#io/evalExpr
62+
[func/evalExpr]: chap-builtin-func.html#func/evalExpr
5163

5264
### false
5365

0 commit comments

Comments
 (0)