def from_ast(cls, filename: str, ast: AstNode, **extras): declarator = next(c for c in ast.children if c.type == AstType.direct_declarator) fn_name = next(c for c in declarator if c.type == AstType.IDENTIFIER).text assert fn_name is not None assert ast.type == AstType.function_definition ret_type = ast[0].text assert ret_type is not None env = Environment.empty() if ret_type != "void": env["ret"] = AtomicType(ret_type) params = declarator[2] if params.type == AstType.parameter_list: for p in params.children: if p.type == AstType.parameter_declaration: ty = p[0].text assert ty is not None and ty in ( "int", "float", "bool", ) ty = AtomicType(ty) name = None if p[1].type == AstType.IDENTIFIER: name = p[1].text elif (p[1].type == AstType.direct_declarator and p[1][0].type == AstType.IDENTIFIER and p[1][1].text == "["): name = p[1][0].text ty = ArrayType(ty) else: assert False assert name is not None env[name] = ty params = env.get_vars() requires = None for s in ast[-1][1].children: if (s.type == AstType.expression_statement and s[0].type == AstType.postfix_expression and s[0][1].type == AstType.paren_left and s[0][0].type == AstType.IDENTIFIER and s[0][0].text == "requires"): requires = Expr.from_ast(s[0][2], env) cfg = create_cfg(ast, requires, env) vars = env.get_vars() for p in params: del vars[p] vars = [Variable(v, t) for v, t in vars.items()] params = [Variable(v, t) for v, t in params.items()] return cls(filename, cfg=cfg, name=fn_name, vars=vars, params=params, **extras)
def setUp(self): data = {'person': {'age': array([20, 10, 35, 55]), 'dead': array([False, True, False, True])}} self.eval_ctx = EvaluationContext(entity_name='person', entities_data=data) self.parse_ctx = { 'person': {'age': Variable('age'), 'dead': Variable('dead')}, '__entity__': 'person' }
def store_result(self, result, context): if isinstance(result, np.ndarray): res_type = result.dtype.type else: res_type = type(result) if self.temporary: target = self.entity.temp_variables else: # we cannot store/cache self.entity.array[self.name] because the # array object can change (eg when enlarging it due to births) target = self.entity.array # TODO: assert type for temporary variables too target_type_idx = type_to_idx[target[self.name].dtype.type] res_type_idx = type_to_idx[res_type] if res_type_idx > target_type_idx: raise Exception( "trying to store %s value into '%s' field which is of " "type %s" % (idx_to_type[res_type_idx].__name__, self.name, idx_to_type[target_type_idx].__name__)) # the whole column is updated target[self.name] = result # invalidate cache period = context.period if isinstance(period, np.ndarray): assert np.isscalar(period) or not period.shape period = int(period) expr_cache.invalidate(period, context.entity_name, Variable(self.entity, self.name))
def make_standard_game(player1_const, player2_const): pl1_token = make_random_token() pl2_token = make_random_token() pl1 = player1_const(pl1_token, 10, 0) pl2 = player2_const(pl2_token, 10, 0) Identity = lambda: Abstraction("x", Variable("x")) async def pure(x, *, game, player_idx): return x action_pure = MonadIOAction("pure", ['x'], pure) async def give_mana(*, game, player_idx): tok = game.players[player_idx].sec_token logging.info(f"Player {tok} gained 10 mana!") game.players[player_idx].mana += 10 return Identity() action_give_mana = MonadIOAction("give_10_mana", [], give_mana) async def do_damage(x, *, game, player_idx): tok = game.players[player_idx].sec_token otok = game.players[~player_idx].sec_token logging.info(f"Player {tok} dealt {x.name} damage to {otok}!") if type(x.name) == int: game.players[~player_idx].health -= x.name else: logging.info(f"Invalid type!") # await self.players[player_idx].tell_msg(f"do_damage needs an int, you gave {type(x.name)}") return Identity() action_do_damage = MonadIOAction("do_damage", ['x'], do_damage) async def get_opponent_health(*, game, player_idx): tok = game.players[player_idx].sec_token logging.info(f"Player {tok} gets opponents health!") return Symbol(game.players[~player_idx].health) action_goh = MonadIOAction("get_opponent_health", [], get_opponent_health) layout = MonadIOLayout([action_pure, action_give_mana, action_do_damage, action_goh]) game = Game( make_random_token(), [pl1, pl2], layout, ) game.add_combinator(1, "pure", game.layout.constructor_for_idx(0)) game.add_combinator(5, "+10 mana", game.layout.constructor_for_idx(1)) game.add_combinator(10, "λx. do x damage", game.layout.constructor_for_idx(2)) game.add_combinator(10, "get opponent's health", game.layout.constructor_for_idx(3)) game.add_combinator(1, "bind", game.layout.constructor_for_idx(4)) game.add_combinator(2, "the number 2", Symbol(2)) game.add_combinator(7, "the number 7", Symbol(7)) return game, pl1_token, pl2_token
def variables(self): if self._variables is None: if self.process_strings: processes = self.process_strings.items() else: processes = [] # names of all processes (hybrid or not) of the entity process_names = set(k for k, v in processes if k is not None) # names of all entity variables (temporary or not) which are set # globally all_predictors = set(self.collect_predictors(processes)) field_names = set(self.fields.names) # normal fields (non-callable/no hybrid variable-function for them) variables = dict((name, Variable(self, name, type_)) for name, type_ in self.fields.name_types if name in field_names - process_names) # callable fields (fields with a process of the same name) variables.update((name, VariableMethodHybrid(self, name, type_)) for name, type_ in self.fields.name_types if name in field_names & process_names) # global temporaries (they are all callable). variables.update((name, VariableMethodHybrid(self, name)) for name in all_predictors - field_names) variables.update(self.links) self._variables = variables return self._variables
def compute(self, context, a, size=None, replace=True, p=None): if isinstance(p, (list, np.ndarray)) and len(p) and not np.isscalar(p[0]): assert len(p) == len(a) assert all(len(px) == size for px in p) assert len(a) >= 2 # I have not found a way to do this without an explicit loop as # np.digitize only supports a 1d array for bins. What we do is # worse than a linear "search" since we always evaluate all # possibilities (there is no shortcut when the value is found). # It might be faster to rewrite this using numba + np.digitize # for each individual (assuming it has a low setup overhead). # if isinstance(p, list) and any(isinstance(px, la.LArray) for px in p): # p = [np.asarray(px) for px in p] ap = np.asarray(p) cdf = ap.cumsum(axis=0) # copied & adapted from numpy/random/mtrand/mtrand.pyx atol = np.sqrt(np.finfo(np.float64).eps) if np.issubdtype(ap.dtype, np.floating): atol = max(atol, np.sqrt(np.finfo(ap.dtype).eps)) if np.any(np.abs(cdf[-1] - 1.) > atol): raise ValueError("probabilities do not sum to 1") cdf /= cdf[-1] # the goal is to build something like: # if(u < proba1, outcome1, # if(u < proba2, outcome2, # outcome3)) data = {'u': np.random.uniform(size=size)} expr = a[-1] # iterate in reverse and skip last pairs = zip(cdf[-2::-1], a[-2::-1]) for i, (proba_x, outcome_x) in enumerate(pairs): data['p%d' % i] = proba_x expr = Where( ComparisonOp('<', Variable(None, 'u'), Variable(None, 'p%d' % i)), outcome_x, expr) local_ctx = context.clone(fresh_data=True, entity_data=data) return expr.evaluate(local_ctx) else: return NumpyRandom.compute(self, context, a, size, replace, p)
def build_regression_expr(self, expr, mult=0.0, error_var=None): if error_var is not None: # expr += error_var expr = BinaryOp('+', expr, Variable(None, error_var)) if mult: # expr += normal(0, 1) * mult expr = BinaryOp('+', expr, BinaryOp('*', Normal(0, 1), mult)) return expr
def get_group_context(context, varnames): ent_name = context['__entity__'] entity = context['__entities__'][ent_name] group_context = context.copy() entity_context = group_context[ent_name].copy() entity_context.update( (name, Variable(entity, name)) for name in varnames) group_context[ent_name] = entity_context return group_context
def constructor_for_idx(self, i): # λa0 ... an. c0 ... cm. ci a0 ... an action = self.actions[i] print("Constructor for", action.symb) result = Variable(action.symb.name) for arg in action.arg_names: result = Application(result, Variable(arg)) for other_action in self.actions[::-1]: result = Abstraction(other_action.symb.name, result) for arg in action.arg_names[::-1]: result = Abstraction(arg, result) result = Opaque(action.symb.name, result) return result
def test_expr_str(self): n = NumberLit(11) m = NumberLit(22) self.assertEqual(str(n), "11") self.assertEqual(str(m), "22") x = Variable("x") self.assertEqual(str(x), "x") e1 = BinaryOp(n, Op.Plus, m) self.assertEqual(str(e1), "(11 + 22)") e2 = BinaryOp(n, Op.Mult, m) self.assertEqual(str(e2), "(11 * 22)") e3 = BinaryOp(e1, Op.Mult, e1) self.assertEqual(str(e3), "((11 + 22) * (11 + 22))") y = Variable("y") self.assertEqual(str(y), "y") e4 = BinaryOp(x, Op.Plus, y) self.assertEqual(str(e4), "(x + y)") e5 = LetIn("x", n, x) self.assertEqual(str(e5), "(let x := 11 in x)") e6 = LetIn("x", n, BinaryOp(x, Op.Plus, x)) self.assertEqual(str(e6), "(let x := 11 in (x + x))")
def _eval_need(self, context, need, expressions, possible_values, expressions_context=None): assert isinstance(need, np.ndarray) if expressions_context is None: expressions_context = context # When given a 0d array, we convert it to 1d. This can happen e.g. for # >>> b = True; x = ne.evaluate('where(b, 0.1, 0.2)') # >>> isinstance(x, np.ndarray) # True # >>> x.shape # () if not need.shape: need = np.array([need]) if isinstance(need, LabeledArray): if not expressions: expressions = [ Variable(expressions_context.entity, name) for name in need.dim_names ] if not possible_values: possible_values = need.pvalues assert isinstance(need, np.ndarray) if len(expressions) != len(possible_values): raise Exception("align() expressions and possible_values " "have different length: %d vs %d" % (len(expressions), len(possible_values))) if 'period' in [str(e) for e in expressions]: period = context.period expressions, possible_values, need = \ kill_axis('period', period, expressions, possible_values, need) # kill any axis where the value is constant for all individuals # satisfying the filter # tokill = [(expr, column[0]) # for expr, column in zip(expressions, columns) # if isconstant(column, filter_value)] # for expr, value in tokill: # expressions, possible_values, need = \ # kill_axis(str(expr), value, expressions, possible_values, # need) return need, expressions, possible_values
def build_expr(self, context): res = None for name, coef in zip(self.names, self.coefs): # XXX: parse expressions instead of only simple Variable? if name != 'constant': # cond_dims = self.cond_dims # cond_exprs = [Variable(context.entity, d) for d in cond_dims] # coef = GlobalArray('__xyz')[name, *cond_exprs] term = _mul(Variable(context.entity, name), coef) else: term = coef if res is None: res = term else: res = _plus(res, term) return res
def primary(self): if self.match(TokenType.FALSE): return Literal(False) elif self.match(TokenType.TRUE): return Literal(True) elif self.match(TokenType.NIL): return Literal(None) elif self.match(TokenType.NUMBER, TokenType.STRING): return Literal(self.previous().literal) elif self.match(TokenType.IDENTIFIER): return Variable(self.previous()) elif self.match(TokenType.LEFT_PAREN): expr = self.expression() self.consume(TokenType.RIGHT_PAREN, "Expected ')' after expression.") return Grouping(expr) raise self.error(self.peek(), "Expected a primary literal or a grouping.")
def execute(self, s): entity = self.entity if entity is None: raise Exception(entity_required) period = self.period if period is None: raise Exception(period_required) entity_name = self.entity.name parse_ctx = self.parse_ctx.copy() local_parse_ctx = parse_ctx[entity_name].copy() # add all currently defined temp_variables because otherwise # local variables (defined within a function) wouldn't be available local_parse_ctx.update((name, Variable(entity, name)) for name in entity.temp_variables.keys()) parse_ctx[entity_name] = local_parse_ctx expr = parse(s, parse_ctx, interactive=True) result = expr_eval(expr, self.eval_ctx) if result is None: print("done.") return result
def primary(self): if self.match(TT.LEFT_PAREN): expr = self.expression() self.consume(TT.RIGHT_PAREN, "Expect ')' after expression.") return Grouping(expr) if self.match(TT.FALSE): return Literal(False) if self.match(TT.TRUE): return Literal(True) if self.match(TT.NONE): return Literal(None) if self.match(TT.INT, TT.FLOAT, TT.STRING): return Literal(self.previous().literal) if self.match(TT.IDENTIFIER): return Variable(self.previous()) if self.match(TT.LAMBDA): return self.lambda_declaration() raise self.error(self.peek(), "Expected expression.")
def create_cfg(self, ast: AstNode) -> CfgNode: if ast.type == AstType.labeled_statement: if ast[0].type == AstType.DEFAULT: statement = self.create_cfg(ast[2]) self.labels.append((None, statement)) return statement else: assert ast[0].type == AstType.CASE value = Expr.from_ast(ast[1], self.env) statement = self.create_cfg(ast[3]) self.labels.append((value, statement)) return statement elif ast.type == AstType.semicolon: return self.next_node elif ast.type == AstType.selection_statement: if ast[0].type == AstType.IF: return CondNode( code_location=ast[2].range, condition=Expr.from_ast(ast[2], self.env), true_br=self.create_cfg(ast[4]), false_br=self.create_cfg(ast[6]) if len(ast.children) == 7 else self.next_node, ) elif ast[0].type == AstType.SWITCH: switch_value = Expr.from_ast(ast[2], self.env) statement_env = self.enter_switch(self.next_node) statement = statement_env.create_cfg(ast[4]) conds: list[CondNode] = [] default = self.next_node for case_value, statement in statement_env.labels: if case_value is None: default = statement continue dummy = DummyNode(None) conds.append( CondNode( None, RelExpr("==", switch_value, case_value), statement, dummy, )) for cond, next_ in zip( conds, cast("list[CfgNode]", conds[1:]) + [default]): cond.false_br = next_ return conds[0] if conds else default else: assert False elif ast.type == AstType.compound_statement: self.open_scope() statements: list[CfgNode] = [] dummies: list[DummyNode] = [] for s in ast[1].children: dummy = DummyNode(None) statement = self.with_next(dummy).create_cfg(s) if statement is not dummy: dummies.append(dummy) statements.append(statement) statements.append(self.next_node) for s, s_next, d in zip(statements, statements[1:], dummies): s.replace(d, s_next, set()) self.close_scope() return statements[0] elif ast.type == AstType.jump_statement: # TODO? handle goto if ast[0].type == AstType.BREAK: assert self.loop_end is not None return self.loop_end elif ast[0].type == AstType.CONTINUE: assert self.loop_start is not None return self.loop_start elif ast[0].type == AstType.RETURN: if len(ast.children) == 3: return AssignmentNode( ast.range, expression=Expr.from_ast(ast[1], self.env), var=Variable("ret", self.env["ret"]), next_node=self.end_node, ) else: return self.end_node else: assert False elif ast.type == AstType.declaration: # TODO: what about "int x, y;" type_ = ast[0].text assert type_ is not None if (ast[1].type == AstType.direct_declarator and ast[1][1].type == AstType.bracket_left): var = ast[1][0].text assert var is not None self.env[var] = ArrayType(AtomicType(type_)) return self.next_node type_ = AtomicType(type_) if ast[1].type == AstType.IDENTIFIER: var = ast[1].text assert var is not None self.env[var] = type_ return self.next_node var = ast[1][0].text assert var is not None value = Expr.from_ast(ast[1][2], self.env) self.env[var] = type_ return AssignmentNode( ast.range, expression=value, var=Variable(self.env.rename(var), type_), next_node=self.next_node, ) elif ast.type == AstType.expression_statement: if (ast[0].type == AstType.postfix_expression and ast[0][1].type == AstType.paren_left and ast[0][0].type == AstType.IDENTIFIER): fn = ast[0][0].text if fn == "assert": assertion = Expr.from_ast(ast[0][2], self.env) remembers = tuple(chain.from_iterable(self.remembers)) assertion = (And(remembers + (assertion, )) if remembers else assertion) return AssertNode( ast.range, assertion=assertion, next_node=self.next_node, ) elif fn == "ensures": assert self.end_node.assertion is None self.end_node.assertion = Expr.from_ast( ast[0][2], self.env) self.end_node.code_location = ast.range return self.next_node elif fn == "requires": assert self.start_node.requires is None self.start_node.requires = Expr.from_ast( ast[0][2], self.env) self.start_node.code_location = ast.range return self.next_node elif fn == "freeze": args = ast[0][2] right = Expr.from_ast(args[2], self.env) assert args[0].type == AstType.IDENTIFIER assert args[0].text is not None self.env[args[0].text] = right.get_type() var = Expr.from_ast(args[0], self.env) assert isinstance(var, Variable) return AssignmentNode(ast.range, right, var, self.next_node) elif fn == "remember": self.remembers[-1].append( Expr.from_ast(ast[0][2], self.env)) return self.next_node elif fn == "assume": return AssumeNode(ast.range, Expr.from_ast(ast[0][2], self.env), self.next_node) else: assert False, f"unknown function {fn}" if ast[0].type == AstType.postfix_expression and ast[0][1].type in ( AstType.INC_OP, AstType.DEC_OP, ): left = Expr.from_ast(ast[0][0], self.env) operator = "+=" if ast[0][1].type == AstType.INC_OP else "-=" value = IntValue(1) elif ast[0].type == AstType.unary_expression and ast[0][0].type in ( AstType.INC_OP, AstType.DEC_OP, ): left = Expr.from_ast(ast[0][1], self.env) operator = "+=" if ast[0][0].type == AstType.INC_OP else "-=" value = IntValue(1) elif ast[0].type != AstType.assignment_expression: # FIXME return self.next_node else: value = Expr.from_ast(ast[0][2], self.env) operator = ast[0][1].text assert operator is not None left = Expr.from_ast(ast[0][0], self.env) # TODO? handle chained assignments? # handle other assignment operators: *= /= %= += -= >>= <<= &= |= ^= if operator != "=": operator = operator[:-1] value = BinaryExpr(operator=operator, lhs=left, rhs=value) if isinstance(left, Variable): return AssignmentNode(ast.range, expression=value, var=left, next_node=self.next_node) elif isinstance(left, ArraySelect): # TODO? what about 2d+ arrays? assert isinstance(left.array, Variable) return AssignmentNode( ast.range, var=left.array, expression=ArrayStore( array=left.array, index=left.index, value=value, ), next_node=self.next_node, ) else: assert False elif ast.type == AstType.iteration_statement: if ast[0].type == AstType.WHILE: self.open_scope() while_node = CondNode( ast[2].range, Expr.from_ast(ast[2], self.env), DummyNode(None), self.next_node, ) while_node.true_br = (self.enter_loop( start=while_node, end=self.next_node).with_next(while_node).create_cfg( ast[4])) self.close_scope() return while_node elif ast[0].type == AstType.DO: self.open_scope() cond = CondNode( ast[4].range, Expr.from_ast(ast[4], self.env), DummyNode(None), self.next_node, ) cond.true_br = (self.enter_loop( start=cond, end=self.next_node).with_next(cond).create_cfg(ast[1])) self.close_scope() return cond.true_br elif ast[0].type == AstType.FOR: self.open_scope() if ast[2].type == AstType.declaration: decl = self.with_next(DummyNode(None)).create_cfg(ast[2]) assert isinstance(decl, AssignmentNode) else: assert ast[2].type == AstType.semicolon decl = None if ast[3].type == AstType.expression_statement: cond = CondNode( ast[3].range, Expr.from_ast(ast[3][0], self.env), DummyNode(None), self.next_node, ) else: assert ast[3].type == AstType.semicolon cond = CondNode( ast[3].range, BoolValue(True), DummyNode(None), self.next_node, ) if decl is not None: decl.next_node = cond if ast[4].type == AstType.paren_right: inc = None else: inc = self.with_next(cond).create_cfg( AstNode(None, AstType.expression_statement, ast[4].range, [ast[4]])) cond.true_br = (self.enter_loop( start=cond, end=self.next_node).with_next( inc or cond).create_cfg(ast[5] if inc is None else ast[6])) self.close_scope() return decl or cond else: assert False else: assert False
def test_parser(self): v1 = "x" self.assertEqual(parse(v1), Variable("x")) v2 = "(x)" self.assertEqual(parse(v2), Variable("x")) v3 = "thisisalongvariable" self.assertEqual(parse(v3), Variable(v3)) e1 = "1" self.assertEqual(parse(e1), NumberLit(1)) e2 = "(1)" self.assertEqual(parse(e2), NumberLit(1)) e3 = "((1))" self.assertEqual(parse(e3), NumberLit(1)) e4 = "(1 + 2)" r4 = BinaryOp( NumberLit(1), Op.Plus, NumberLit(2)) self.assertEqual(parse(e4), r4) e5 = "1 + 2" self.assertEqual(parse(e5), r4) e6 = "(1 * 2)" r6 = BinaryOp( NumberLit(1), Op.Mult, NumberLit(2)) self.assertEqual(parse(e6), r6) e7 = "1 * 2" self.assertEqual(parse(e7), r6) e8 = "1 + 2 * 3" r8 = BinaryOp( NumberLit(1), Op.Plus, BinaryOp( NumberLit(2), Op.Mult, NumberLit(3) )) self.assertEqual(parse(e8), r8) e9 = "let x := 1 in x end" r9 = LetIn( "x", NumberLit(1), Variable("x") ) self.assertEqual(parse(e9), r9) e10 = "2 + let x := 1 in x end" r10 = BinaryOp( NumberLit(2), Op.Plus, LetIn( "x", NumberLit(1), Variable("x") )) self.assertEqual(parse(e10), r10) e11 = "let x := 1 in x + 2 end" r11 = LetIn( "x", NumberLit(1), BinaryOp( Variable("x"), Op.Plus, NumberLit(2) )) self.assertEqual(parse(e11), r11) e12 = "let x := 1 in x end + 2" r12 = BinaryOp( LetIn( "x", NumberLit(1), Variable("x")), Op.Plus, NumberLit(2) ) self.assertEqual(parse(e12), r12)
async def run(self, game, player_idx): player = game.players[player_idx] player.deck.append(Variable(self.var_name))
def compute(self, context, *args, **kwargs): filter_value = kwargs.pop('filter', None) missing = kwargs.pop('missing', None) # periods = kwargs.pop('periods', None) header = kwargs.pop('header', True) limit = kwargs.pop('limit', None) entity = context.entity if args: expressions = list(args) else: # extra=False because we don't want globals nor "system" variables # (nan, period, __xxx__) # FIXME: we should also somehow "traverse" expressions in this case # too (args is ()) => all keys in the current context expressions = [ Variable(entity, name) for name in context.keys(extra=False) ] str_expressions = [str(e) for e in expressions] if 'id' not in str_expressions: str_expressions.insert(0, 'id') expressions.insert(0, Variable(entity, 'id')) id_pos = 0 else: id_pos = str_expressions.index('id') # if (self.periods is not None and len(self.periods) and # 'period' not in str_expressions): # str_expressions.insert(0, 'period') # expressions.insert(0, Variable('period')) # id_pos += 1 columns = [] for expr in expressions: if filter_value is False: # dtype does not matter much expr_value = np.empty(0) else: expr_value = expr_eval(expr, context) if (filter_value is not None and isinstance(expr_value, np.ndarray) and expr_value.shape): expr_value = expr_value[filter_value] columns.append(expr_value) ids = columns[id_pos] if isinstance(ids, np.ndarray) and ids.shape: numrows = len(ids) else: # FIXME: we need a test for this case (no idea how this can happen) numrows = 1 # expand scalar columns to full columns in memory # TODO: handle or explicitly reject columns wh ndim > 1 for idx, col in enumerate(columns): dtype = None if not isinstance(col, np.ndarray): dtype = type(col) elif not col.shape: dtype = col.dtype.type if dtype is not None: # TODO: try using itertools.repeat instead as it seems to be a # bit faster and would consume less memory (however, it might # not play very well with Pandas.to_csv) newcol = np.full(numrows, col, dtype=dtype) columns[idx] = newcol if limit is not None: assert isinstance(limit, (int, long)) columns = [col[:limit] for col in columns] data = izip(*columns) table = chain([str_expressions], data) if header else data return PrettyTable(table, missing)
c_pure, c_print, c_read, c_xyz, c_bind = [ CONSOLE_IO.constructor_for_idx(i) for i in range(5) ] print(c_xyz) program = C( c_bind, c_read, λ( "a", C( c_bind, c_read, λ( "b", C( c_bind, c_read, λ( "c", C( c_xyz, Variable("a"), Variable("b"), Variable("c"), ))))))) print(program) print(CONSOLE_EIO(program)) print(CONSOLE_IO.constructor_for_idx(3))
def mprint(x): print("Monadic print:", x) return Abstraction("x", Variable("x"))
def mxyz(x, y, z): print(f"x = {x}, y = {y}, z = {z}") return Abstraction("x", Variable("x"))