Source code for WDL.Expr

"""
WDL expressions composing literal values, arithmetic, comparison, conditionals,
string interpolation, arrays & maps, and function applications. These appear on
the right-hand side of value declarations and in task command substitutions,
task runtime sections, and workflow scatter and conditional sections.

The abstract syntax tree (AST) for any expression is represented by an instance
of a Python class deriving from ``WDL.Expr.Base``. Any such node may have other
nodes attached "beneath" it. An expression can be evaluated to a ``Value``
given a suitable ``WDL.Env.Bindings[Value.Base]``.

.. inheritance-diagram:: WDL.Expr
"""
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Tuple, Union, Iterable
import regex
from .Error import SourcePosition, SourceNode
from . import Type, Value, Env, Error, StdLib


[docs]class Base(SourceNode, ABC): """Superclass of all expression AST nodes""" _type: Optional[Type.Base] = None _check_quant: bool = True _stdlib: "Optional[StdLib.Base]" = None _struct_types: Optional[Env.Bindings[Dict[str, Type.Base]]] = None @property def type(self) -> Type.Base: """ :type: WDL.Type.Base WDL type of this expression. Undefined on construction; populated by one invocation of ``infer_type``. """ # Failure of this assertion indicates use of an Expr object without # first calling _infer_type assert self._type is not None return self._type @abstractmethod def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: # Abstract protected method called by infer_type(): return the inferred # type with no side-effects, obeying self._check_quant. pass
[docs] def infer_type( self, type_env: Env.Bindings[Type.Base], stdlib: StdLib.Base, check_quant: bool = True, struct_types: Optional[Env.Bindings[Dict[str, Type.Base]]] = None, ) -> "Base": """infer_type(self, type_env : Env.Bindings[Type.Base], stdlib : StdLib.Base) -> WDL.Expr.Base Infer the expression's type within the given type environment. Must be invoked exactly once prior to use of other methods. :param stdlib: a context-specific standard function library for typechecking :param check_quant: when ``False``, disables static validation of the optional (?) type quantifier when `typecheck()` is called on this expression, so for example type ``T?`` can satisfy an expected type ``T``. Applies recursively to the type inference and checking of any sub-expressions. :raise WDL.Error.StaticTypeMismatch: when the expression fails to type-check :return: `self` """ # Failure of this assertion indicates multiple invocations of # infer_type assert self._type is None # recursive descent into child expressions with Error.multi_context() as errors: for child in self.children: assert isinstance(child, Base) errors.try1( lambda child=child: child.infer_type( type_env, stdlib, check_quant, struct_types ) ) # invoke derived-class logic. we pass check_quant, stdlib, and struct_types hackily through # instance variables since only some subclasses use them. self._check_quant = check_quant self._stdlib = stdlib self._struct_types = struct_types try: self._type = self._infer_type(type_env) finally: self._stdlib = None self._struct_types = None assert self._type and isinstance(self.type, Type.Base) return self
[docs] def typecheck(self, expected: Type.Base) -> "Base": """typecheck(self, expected : Type.Base) -> WDL.Expr.Base Check that this expression's type is, or can be coerced to, ``expected``. :raise WDL.Error.StaticTypeMismatch: :return: `self` """ try: self.type.check(expected, self._check_quant) except TypeError as exn: raise Error.StaticTypeMismatch( self, expected, self.type, exn.args[0] if exn.args else "" ) return self
@abstractmethod def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: # to be overridden by subclasses. eval() calls this and deals with any # exceptions raised pass
[docs] def eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: """ Evaluate the expression in the given environment :param stdlib: a context-specific standard function library implementation """ try: ans = self._eval(env, stdlib) ans.expr = self return ans except Error.RuntimeError: raise except Exception as exn: raise Error.EvalError(self, str(exn)) from exn
@property def literal(self) -> Optional[Value.Base]: """ If the expression is a literal constant, return its value; otherwise return None. The result can be an instance of ``WDL.Value.Null`` which is distinct from None. """ if isinstance(self, (Boolean, Int, Float)): return self._eval(Env.Bindings(), None) # pyre-fixme return None
[docs]class Boolean(Base): """ Boolean literal """ value: bool """ :type: bool Literal value """ def __init__(self, pos: SourcePosition, literal: bool) -> None: super().__init__(pos) self.value = literal def __str__(self): return str(self.value).lower() def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: return Type.Boolean() def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Boolean: """""" return Value.Boolean(self.value)
[docs]class Int(Base): """ Integer literal """ value: int """ :type: int Literal value """ def __init__(self, pos: SourcePosition, literal: int) -> None: super().__init__(pos) self.value = literal def __str__(self): return str(self.value) def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: return Type.Int() def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Int: """""" return Value.Int(self.value)
# Float literal
[docs]class Float(Base): """ Numeric literal """ value: float """ :type: float Literal value """ def __init__(self, pos: SourcePosition, literal: float) -> None: super().__init__(pos) self.value = literal def __str__(self): return str(self.value) def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: return Type.Float() def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Float: """""" return Value.Float(self.value)
[docs]class Null(Base): """ WDL ``None`` literal (called ``Null`` to avoid conflict with Python ``None``) """ value: None """ :type: None """ def __init__(self, pos: SourcePosition) -> None: super().__init__(pos) self.value = None def __str__(self): return "None" def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: return Type.Any(null=True) def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Null: """""" return Value.Null()
[docs]class Placeholder(Base): """Holds an expression interpolated within a string or command""" options: Dict[str, str] """ :type: Dict[str,str] Placeholder options (sep, true, false, default)""" expr: Base """ :type: WDL.Expr.Base Expression to be evaluated and substituted """ def __init__(self, pos: SourcePosition, options: Dict[str, str], expr: Base) -> None: super().__init__(pos) self.options = options self.expr = expr # preprocess expr to rewrite any Apply("_add") to Apply("_interpolation_add") for the # special interpolation-only behavior of + for String? operands. def rewrite_adds(ch: Base): if isinstance(ch, Apply) and ch.function_name == "_add": ch.function_name = "_interpolation_add" for ch2 in ch.children: rewrite_adds(ch2) rewrite_adds(self.expr) def __str__(self): options = [] for option in self.options: options.append('{}="{}"'.format(option, self.options[option])) options.append(str(self.expr)) return "~{{{}}}".format(" ".join(options)) @property def children(self) -> Iterable[SourceNode]: yield self.expr def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: if isinstance(self.expr.type, Type.Array): if "sep" not in self.options: raise Error.IncompatibleOperand( self, "provide `sep'arator string to interpolate array items" ) # if sum(1 for t in [Type.Int, Type.Float, Type.Boolean, Type.String, Type.File] if isinstance(self.expr.type.item_type, t)) == 0: # noqa # raise Error.StaticTypeMismatch(self, Type.Array(Type.Any()), self.expr.type, "cannot use array of complex types for command placeholder") # noqa elif "sep" in self.options: raise Error.StaticTypeMismatch( self, Type.Array(Type.Any()), self.expr.type, "command placeholder has 'sep' option for non-Array expression", ) if "true" in self.options or "false" in self.options: if not isinstance(self.expr.type, Type.Boolean): raise Error.StaticTypeMismatch( self, Type.Boolean(), self.expr.type, "command placeholder 'true' and 'false' options used with non-Boolean expression", ) if not ("true" in self.options and "false" in self.options): raise Error.StaticTypeMismatch( self, Type.Boolean(), self.expr.type, "command placeholder with only one of 'true' and 'false' options", ) return Type.String() def _eval_impl(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.String: """""" v = self.expr.eval(env, stdlib) if isinstance(v, Value.Null): if "default" in self.options: return Value.String(self.options["default"]) return Value.String("") if isinstance(v, Value.String): return v if isinstance(v, Value.Array): return Value.String( self.options["sep"].join(item.coerce(Type.String()).value for item in v.value) ) if v == Value.Boolean(True) and "true" in self.options: return Value.String(self.options["true"]) if v == Value.Boolean(False) and "false" in self.options: return Value.String(self.options["false"]) return Value.String(str(v)) def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.String: ans = self._eval_impl(env, stdlib) placeholder_regex: regex.Pattern = getattr(stdlib, "_placeholder_regex", None) if placeholder_regex and not placeholder_regex.fullmatch(ans.value): raise Error.InputError( "Task command placeholder value forbidden by configuration [task_runtime] placeholder_regex" ) return ans
[docs]class String(Base): """String literal, possibly interleaved with expression placeholders for interpolation""" parts: List[Union[str, Placeholder]] """ :type: List[Union[str,WDL.Expr.Placeholder]] The parts list begins and ends with the original delimiters (quote marks, braces, or triple angle brackets). Between these is a sequence of literal strings and/or interleaved placeholder expressions. Escape sequences in the literals will NOT have been decoded (although the parser will have checked they're valid). Strings arising from task commands leave escape sequences to be interpreted by the shell in the task container. Other string literals have their escape sequences interpreted upon evaluation to string values. """ command: bool """ :type: bool True if this expression is a task command template, as opposed to a string expression anywhere else. Controls whether backslash escape sequences are evaluated or (for commands) passed through for shell interpretation. """ def __init__( self, pos: SourcePosition, parts: List[Union[str, Placeholder]], command: bool = False ) -> None: super().__init__(pos) self.parts = parts self.command = command def __str__(self): parts = [] for part in self.parts: if isinstance(part, Placeholder): parts.append(str(part)) else: parts.append(part) return "".join(parts) @property def children(self) -> Iterable[SourceNode]: for p in self.parts: if isinstance(p, Base): yield p def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: return Type.String() def typecheck(self, expected: Optional[Type.Base]) -> Base: """""" return super().typecheck(expected) # pyre-ignore def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.String: """""" ans = [] for part in self.parts: if isinstance(part, Placeholder): # evaluate interpolated expression & stringify ans.append(part.eval(env, stdlib).value) elif isinstance(part, str): if self.command: ans.append(part) else: from ._parser import decode_escapes # avoiding circular import ans.append(decode_escapes(self.pos, part)) else: assert False # concatenate the stringified parts and trim the surrounding quotes # TODO: make command repr include delimiters for consistency if self.command: return Value.String("".join(ans)) delim = self.parts[0] assert delim in ("'", '"', "{", "<<<") delim2 = self.parts[-1] assert delim2 in ("'", '"', "}", ">>>") and len(delim) == len(delim2) return Value.String("".join(ans)[len(delim) : -len(delim)]) @property def literal(self) -> Optional[Value.Base]: if next((p for p in self.parts if not isinstance(p, str)), None): return None return self._eval(Env.Bindings(), None) # pyre-fixme
[docs]class Array(Base): """ Array literal """ items: List[Base] """ :type: List[WDL.Expr.Base] Expression for each item in the array literal """ def __init__(self, pos: SourcePosition, items: List[Base]) -> None: super(Array, self).__init__(pos) self.items = items def __str__(self): items = [] for item in self.items: items.append(str(item)) return "[{}]".format(", ".join(items)) @property def children(self) -> Iterable[SourceNode]: for it in self.items: yield it def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: if not self.items: return Type.Array(Type.Any()) item_type = Type.unify( [item.type for item in self.items], check_quant=self._check_quant, force_string=True ) if isinstance(item_type, Type.Any) and not item_type.optional: raise Error.IndeterminateType(self, "unable to unify array item types") return Type.Array(item_type, optional=False, nonempty=True) def typecheck(self, expected: Optional[Type.Base]) -> Base: """""" if not self.items and isinstance(expected, Type.Array): # the literal empty array satisfies any array type return self return super().typecheck(expected) # pyre-ignore def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Array: """""" assert isinstance(self.type, Type.Array) return Value.Array( self.type.item_type, [item.eval(env, stdlib).coerce(self.type.item_type) for item in self.items], ) @property def literal(self) -> Optional[Value.Base]: assert isinstance(self.type, Type.Array) ans = [] for item in self.items: item_literal = item.literal if item_literal: ans.append(item_literal.coerce(self.type.item_type)) else: return None return Value.Array(self.type.item_type, ans)
[docs]class Pair(Base): """ Pair literal """ left: Base """ :type: WDL.Expr.Base Left-hand expression in the pair literal """ right: Base """ :type: WDL.Expr.Base Right-hand expression in the pair literal """ def __init__(self, pos: SourcePosition, left: Base, right: Base) -> None: super().__init__(pos) self.left = left self.right = right def __str__(self): return "({}, {})".format(str(self.left), str(self.right)) @property def children(self) -> Iterable[SourceNode]: yield self.left yield self.right def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: return Type.Pair(self.left.type, self.right.type) def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: """""" assert isinstance(self.type, Type.Pair) lv = self.left.eval(env, stdlib) rv = self.right.eval(env, stdlib) return Value.Pair(self.left.type, self.right.type, (lv, rv)) @property def literal(self) -> Optional[Value.Base]: assert isinstance(self.type, Type.Pair) lv = self.left.literal rv = self.right.literal if lv and rv: return Value.Pair(self.left.type, self.right.type, (lv, rv)) return None
[docs]class Map(Base): """ Map literal """ items: List[Tuple[Base, Base]] """ :type: List[Tuple[WDL.Expr.Base,WDL.Expr.Base]] Expressions for the map literal keys and values """ def __init__(self, pos: SourcePosition, items: List[Tuple[Base, Base]]) -> None: super().__init__(pos) self.items = items def __str__(self): items = [] for item in self.items: items.append("{}: {}".format(str(item[0]), str(item[1]))) return "{{{}}}".format(", ".join(items)) @property def children(self) -> Iterable[SourceNode]: for k, v in self.items: yield k yield v def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: if not self.items: return Type.Map((Type.Any(), Type.Any()), literal_keys=set()) kty = Type.unify([k.type for (k, _) in self.items], check_quant=self._check_quant) if isinstance(kty, Type.Any): raise Error.IndeterminateType(self, "unable to unify map key types") vty = Type.unify( [v.type for (_, v) in self.items], check_quant=self._check_quant, force_string=True ) if isinstance(vty, Type.Any): raise Error.IndeterminateType(self, "unable to unify map value types") literal_keys = None if kty == Type.String(): # If the keys are string constants, record them in the Type object # for potential later use in struct coercion. (Normally the Type # encodes the common type of the keys, but not the keys themselves) literal_keys = set() for k, _ in self.items: if ( literal_keys is not None and isinstance(k, String) and len(k.parts) == 3 and isinstance(k.parts[1], str) ): literal_keys.add(k.parts[1]) else: literal_keys = None return Type.Map((kty, vty), literal_keys=literal_keys) def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: """""" assert isinstance(self.type, Type.Map) keystrs = set() eitems = [] for k, v in self.items: ek = k.eval(env, stdlib) sk = str(ek) if sk in keystrs: raise Error.EvalError(self, "duplicate keys in Map literal") eitems.append((ek, v.eval(env, stdlib))) keystrs.add(sk) return Value.Map(self.type.item_type, eitems) @property def literal(self) -> Optional[Value.Base]: assert isinstance(self.type, Type.Map) items = [] for k, v in self.items: kl = k.literal vl = v.literal if kl and vl: items.append((kl, vl)) else: return None return Value.Map(self.type.item_type, items)
[docs]class Struct(Base): """ Struct literal """ members: Dict[str, Base] """ :type: Dict[str,WDL.Expr.Base] The struct literal is modelled initially as a bag of keys and values, which can be coerced to a specific struct type during typechecking. """ struct_type_name: Optional[str] """ :type: Optional[str] In WDL 2.0+ each struct literal may specify the intended struct type name. """ def __init__( self, pos: SourcePosition, members: List[Tuple[str, Base]], struct_type_name: Optional[str] = None, ): super().__init__(pos) self.members = {} for k, v in members: if k in self.members: raise Error.MultipleDefinitions(self.pos, "duplicate keys " + k) self.members[k] = v self.struct_type_name = struct_type_name assert struct_type_name is None or isinstance(struct_type_name, str), str(struct_type_name) def __str__(self): members = [] for member in self.members: members.append('"{}": {}'.format(member, str(self.members[member]))) # Returns a Map literal instead of a struct literal as these are version dependant return "{{{}}}".format(", ".join(members)) @property def children(self) -> Iterable[SourceNode]: return self.members.values() def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: object_type = Type.Object({k: v.type for k, v in self.members.items()}) if not self.struct_type_name: # pre-WDL 2.0: object literal with deferred typechecking return object_type # resolve struct type struct_type_members = None if self._struct_types and self.struct_type_name in self._struct_types: struct_type_members = self._struct_types[self.struct_type_name] if struct_type_members is None: raise Error.InvalidType(self, "Unknown type " + self.struct_type_name) struct_type = Type.StructInstance(self.struct_type_name) struct_type.members = struct_type_members # typecheck members vs struct declaration try: object_type.check(struct_type, self._check_quant) except TypeError as exn: raise Error.StaticTypeMismatch( self, struct_type, object_type, exn.args[0] if exn.args else "" ) return struct_type def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: assert isinstance(self.type, (Type.Object, Type.StructInstance)) ans = {} for k, v in self.members.items(): ans[k] = v.eval(env, stdlib) if isinstance(self.type, Type.StructInstance): assert self.type.members ans[k] = ans[k].coerce(self.type.members[k]) return Value.Struct(self.type, ans) @property def literal(self) -> Optional[Value.Base]: ans = {} for k, v in self.members.items(): vl = v.literal if vl: ans[k] = vl else: return None assert isinstance(self.type, (Type.Object, Type.StructInstance)) return Value.Struct(self.type, ans)
[docs]class IfThenElse(Base): """ Ternary conditional expression """ condition: Base """ :type: WDL.Expr.Base A Boolean expression for the condition """ consequent: Base """ :type: WDL.Expr.Base Expression evaluated when the condition is true """ alternative: Base """ :type: WDL.Expr.Base Expression evaluated when the condition is false """ def __init__( self, pos: SourcePosition, condition: Base, consequent: Base, alternative: Base ) -> None: super().__init__(pos) self.condition = condition self.consequent = consequent self.alternative = alternative def __str__(self): return "if {} then {} else {}".format( str(self.condition), str(self.consequent), str(self.alternative) ) @property def children(self) -> Iterable[SourceNode]: yield self.condition yield self.consequent yield self.alternative def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: # check for Boolean condition if self.condition.type != Type.Boolean(): raise Error.StaticTypeMismatch( self, Type.Boolean(), self.condition.type, "in if condition" ) ty = Type.unify( [self.consequent.type, self.alternative.type], check_quant=self._check_quant ) if isinstance(ty, Type.Any): raise Error.StaticTypeMismatch( self, self.consequent.type, self.alternative.type, "(unable to unify consequent & alternative types)", ) return ty def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: """""" if self.condition.eval(env, stdlib).expect(Type.Boolean()).value: ans = self.consequent.eval(env, stdlib) else: ans = self.alternative.eval(env, stdlib) return ans
[docs]class Ident(Base): """ An identifier referencing a named value or call output. ``Ident`` nodes are wrapped in ``Get`` nodes, as discussed below. """ name: str """:type: str Name, possibly including a dot-separated namespace """ referee: "Union[None, Tree.Decl, Tree.Call, Tree.Scatter, Tree.Gather]" """ After typechecking within a task or workflow, stores the AST node to which the identifier refers: a ``WDL.Tree.Decl`` for value references; a ``WDL.Tree.Call`` for call outputs; a ``WDL.Tree.Scatter`` for scatter variables; or a ``WDL.Tree.Gather`` object representing a value or call output that resides within a scatter or conditional section. """ def __init__(self, pos: SourcePosition, name: str) -> None: super().__init__(pos) assert name and not name.endswith(".") and not name.startswith(".") and ".." not in name self.name = name self.referee = None def __str__(self): return self.name @property def children(self) -> Iterable[SourceNode]: return [] def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: # The following Env.resolve will never fail, as Get._infer_type does # the heavy lifting for us. b = type_env.resolve_binding(self.name) ans = b.value # referee comes from the type environment's info value referee = b.info if referee: assert referee.__class__.__name__ in [ "Decl", "Call", "Scatter", "Gather", ], referee.__class__.__name__ self.referee = referee return ans def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: """""" return env[self.name] @property def _ident(self) -> str: return self.name
class _LeftName(Base): # This AST node is a placeholder involved in disambiguating dot-separated # identifiers (e.g. "leftname.midname.rightname") as elaborated in the Get # docstring below. The parser, lacking the context to resolve this syntax, # creates this node simply to represent the leftmost (sometimes only) name, # as the innard of a Get node, potentially (not necessarily) with a # member name. Later during typechecking, Get._infer_type folds _LeftName # into an `Ident` expression; the library user should never have to work # with _LeftName. name: str def __init__(self, pos: SourcePosition, name: str) -> None: super().__init__(pos) assert name self.name = name def __str__(self): return self.name def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: raise NotImplementedError() def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: raise NotImplementedError() @property def _ident(self) -> str: return self.name
[docs]class Get(Base): """ AST node representing access to a value by identifier (including namespaced ones), or accessing a member of a pair or struct as ``.member``. The entaglement of these two cases is inherent in WDL. Consider the syntax ``leftname.midname.rightname``. One interpretation is that ``leftname`` is an identifier for a struct value, and ``.midname.rightname`` represents a chain of struct member accesses. But another possibility is that ``leftname`` is a call, ``midname`` is a struct output of that call, and ``rightname`` is a member of that struct. These cases can't be distinguished by the syntax parser alone, but must be resolved during typechecking with reference to the calls and identifiers available in the environment. The typechecker does conveniently resolve such cases, and to minimize the extent to which it has to restructure the AST in doing so, all identifiers (with or without a namespace) are represented as a ``Get`` node wrapping an ``Ident`` node. The ``Get`` node may specify a member name to access, but may not if the identifier is to be accessed directly. On the other hand, the expression inside a ``Get`` node need not be a simple identifier, e.g. ``arr[1].memb.left`` is be represented as: ``Get(Get(Apply("_at", Get(Ident("arr")), 1),"memb"),"left")`` """ expr: Base """ :type: WDL.Expr.Base The expression whose value is accessed """ member: Optional[str] """ :type: Optional[str] If the expression is accessing a pair/struct member, then ``expr.type`` is ``WDL.Type.Pair`` or ``WDL.Type.StructInstance`` and this field gives the desired member name (``left`` or ``right`` for pairs). Otherwise the expression accesses ``expr`` directly, and ``member`` is ``None``. """ def __init__(self, pos: SourcePosition, expr: Base, member: Optional[str]) -> None: super().__init__(pos) assert expr self.expr = expr self.member = member def __str__(self): if self.member is not None: return "{}.{}".format(str(self.expr), self.member) return str(self.expr) @property def children(self) -> Iterable[SourceNode]: if self._type: # suppress children until resolution/typechecking is complete yield self.expr def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: if isinstance(self.expr, _LeftName): # expr is a lone "name" -- try to resolve it as an identifier, # and if that works, transform it to Ident("name") if self.expr.name in type_env: self.expr = Ident(self.expr.pos, self.expr.name) elif not self.member: raise Error.UnknownIdentifier(self) # attempt to typecheck expr, disambiguating whether it's an # intermediate value, a resolvable identifier, or neither assert self._stdlib is not None try: self.expr.infer_type(type_env, self._stdlib, self._check_quant) except Error.UnknownIdentifier: # Fail...there's one case we may be able to rescue, where expr is a # _LeftName inside zero or more Gets representing an incomplete # namespaced identifier, and our member completes the path to an # available named value. if not (isinstance(self.expr, (_LeftName, Get)) and self.expr._ident and self.member): raise # attempt to resolve "expr.member" and if that works, transform # expr to Ident("expr.member") if self.expr._ident + "." + self.member not in type_env: message = None if type_env.has_namespace(self.expr._ident): # specific error message when namespace exists but member doesn't message = f"No {self.member} in namespace {self.expr._ident}" raise Error.UnknownIdentifier(self, message=message) from None self.expr = Ident(self.pos, self._ident) self.expr.infer_type(type_env, self._stdlib, self._check_quant) self.member = None # now we've typechecked expr ety = self.expr.type assert ety if not self.member: # no member to access; just propagate expr type assert isinstance(self.expr, Ident) return ety # now we expect expr to be a pair or struct, whose member we're # accessing if not isinstance(ety, (Type.Pair, Type.StructInstance)): raise Error.NoSuchMember(self, self.member) if self._check_quant and ety.optional: raise Error.StaticTypeMismatch(self.expr, ety.copy(optional=False), ety) if self.member in ["left", "right"]: if isinstance(ety, Type.Pair): return ety.left_type if self.member == "left" else ety.right_type raise Error.NoSuchMember(self, self.member) if isinstance(ety, Type.StructInstance): try: assert ety.members is not None return ety.members[self.member] except KeyError: pass raise Error.NoSuchMember(self, self.member) def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: innard_value = self.expr.eval(env, stdlib) if not self.member: return innard_value if isinstance(innard_value, Value.Pair): assert self.member in ["left", "right"] return innard_value.value[0 if self.member == "left" else 1] if isinstance(innard_value, Value.Struct): return innard_value.value[self.member] raise NotImplementedError() @property def _ident(self) -> str: # helper for the resolution logic above -- get the partial identifier # recursing into nested Gets, if there's a _LeftName at the bottom. if isinstance(self.expr, (_LeftName, Get)) and self.expr._ident: return self.expr._ident + (("." + self.member) if self.member else "") return ""
def _add_parentheses(arguments, parent_operator): """ Add parentheses around arguments if necessary. Adds parentheses around if-then-else clauses if on the left side of the parent operator (otherwise it is ambiguous whether 'if true then 1 else 100 + 1' should return 1 or 2). Adds parentheses around expression with a lower precedence than the parent operator """ arguments_out = [] precedence = { "_mul": 7, "_div": 7, "_rem": 7, "_add": 6, "_interpolation_add": 6, "_sub": 6, "_lt": 5, "_lte": 5, "_gt": 5, "_gte": 5, "_eqeq": 4, "_neq": 4, "_land": 3, "_lor": 3, } for i, argument in enumerate(arguments): if isinstance(argument, IfThenElse) and (parent_operator in precedence and i == 0): arguments_out.append("({})".format(str(argument))) elif isinstance(argument, Apply): if precedence.get(parent_operator, 100) > precedence.get(argument.function_name, 100): arguments_out.append("({})".format(str(argument))) else: arguments_out.append(str(argument)) else: arguments_out.append(str(argument)) return arguments_out
[docs]class Apply(Base): """Application of a built-in or standard library function""" function_name: str """Name of the function applied :type: str""" arguments: List[Base] """ :type: List[WDL.Expr.Base] Expressions for each function argument """ def __init__(self, pos: SourcePosition, function: str, arguments: List[Base]) -> None: super().__init__(pos) self.function_name = function self.arguments = arguments def __str__(self): arguments = _add_parentheses(self.arguments, self.function_name) infix = { "_mul": "*", "_div": "/", "_rem": "%", "_add": "+", "_interpolation_add": "+", "_sub": "-", "_lt": "<", "_lte": "<=", "_gt": ">", "_gte": ">=", "_eqeq": "==", "_neq": "!=", "_land": "&&", "_lor": "||", } if self.function_name in infix: return "{} {} {}".format(arguments[0], infix[self.function_name], arguments[1]) elif self.function_name == "_at": return "{}[{}]".format(arguments[0], arguments[1]) elif self.function_name == "_negate": return "!{}".format(arguments[0]) else: return "{}({})".format(self.function_name, ",".join(arguments)) @property def children(self) -> Iterable[SourceNode]: for arg in self.arguments: yield arg def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base: f = getattr(self._stdlib, self.function_name, None) if not f: raise Error.NoSuchFunction(self, self.function_name) from None assert isinstance(f, StdLib.Function) return f.infer_type(self) def _eval(self, env: Env.Bindings[Value.Base], stdlib: StdLib.Base) -> Value.Base: """""" f = getattr(stdlib, self.function_name, None) assert isinstance(f, StdLib.Function) return f(self, env, stdlib)