printers.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. import sys
  2. from typing import Optional
  3. import sympy
  4. from sympy.printing.precedence import PRECEDENCE, precedence
  5. from sympy.printing.str import StrPrinter
  6. INDEX_TYPE = "int64_t"
  7. INDEX_TYPE_MAX = (1 << 63) - 1
  8. INDEX_TYPE_MIN = -1 << 63
  9. # This printer contains rules that are supposed to be generic for both C/C++ and
  10. # Python
  11. class ExprPrinter(StrPrinter):
  12. # override this so that _print_FloorDiv is used
  13. printmethod = "_torch_sympystr"
  14. def _print_Mul(self, expr: sympy.Expr) -> str:
  15. return self.stringify(expr.args, "*", precedence(expr))
  16. def _print_Not(self, expr: sympy.Expr) -> str:
  17. return f"not ({self._print(expr.args[0])})"
  18. def _print_Add(self, expr: sympy.Expr, order: Optional[str] = None) -> str:
  19. return self.stringify(expr.args, " + ", precedence(expr))
  20. def _print_Relational(self, expr: sympy.Expr) -> str:
  21. return self.stringify(expr.args, f" {expr.rel_op} ", precedence(expr))
  22. def _print_BitwiseFn_bitwise_and(self, expr: sympy.Expr) -> str:
  23. return self.stringify(expr.args, " & ", PRECEDENCE["BitwiseAnd"])
  24. def _print_BitwiseFn_bitwise_or(self, expr: sympy.Expr) -> str:
  25. return self.stringify(expr.args, " | ", PRECEDENCE["BitwiseOr"])
  26. # NB: this is OK to put here, because Mod is only defined for positive
  27. # numbers, and so across C/Python its behavior is consistent
  28. def _print_Mod(self, expr: sympy.Expr) -> str:
  29. return self.stringify(expr.args, " % ", PRECEDENCE["Atom"] - 0.5)
  30. def _print_FloatTrueDiv(self, expr: sympy.Expr) -> str:
  31. s = self.stringify(expr.args, " / ", PRECEDENCE["Atom"] - 0.5)
  32. return f"({s})"
  33. def _print_CleanDiv(self, expr: sympy.Expr) -> str:
  34. return self._print_FloorDiv(expr)
  35. def _print_Identity(self, expr: sympy.Expr) -> str:
  36. return self._print(expr.args[0])
  37. def _print_Float(self, expr: sympy.Expr) -> str:
  38. if expr._prec == 53:
  39. # IEEE-754 double precision have 53 bits. SymPy prints them with
  40. # 15 digits, but we need 17 for round-trip correctness
  41. return str(sympy.Float(expr, dps=17))
  42. else:
  43. # We don't use other precisions in pytorch
  44. return str(expr)
  45. # This must be implemented because sympy will collect x * x into Pow(x, 2), without
  46. # any explicit intervention. We print it just like x * x, notably, we
  47. # never generate sympy.Pow with floats.
  48. #
  49. # NB: this pow by natural, you should never have used builtin sympy.pow
  50. # for FloatPow, and a symbolic exponent should be PowByNatural. These
  51. # means exp is guaranteed to be integer.
  52. def _print_Pow(self, expr: sympy.Expr) -> str:
  53. base, exp = expr.args
  54. assert exp == int(exp), exp
  55. exp = int(exp)
  56. assert exp >= 0
  57. if exp > 0:
  58. return self.stringify([base] * exp, "*", PRECEDENCE["Mul"])
  59. return "1"
  60. # Explicit NotImplemented functions are to prevent default sympy printing
  61. # behavior, which will just barf out ToFloat(...) to your IR. The error
  62. # message is better here because it tells you which printer class it needs
  63. # to go in.
  64. def _print_ToFloat(self, expr: sympy.Expr) -> str:
  65. raise NotImplementedError(f"_print_ToFloat not implemented for {type(self)}")
  66. def _print_Infinity(self, expr: sympy.Expr) -> str:
  67. raise NotImplementedError(f"_print_Infinity not implemented for {type(self)}")
  68. def _print_NegativeInfinity(self, expr: sympy.Expr) -> str:
  69. raise NotImplementedError(
  70. f"_print_NegativeInfinity not implemented for {type(self)}"
  71. )
  72. def _print_FloorDiv(self, expr: sympy.Expr) -> str:
  73. raise NotImplementedError(f"_print_FloorDiv not implemented for {type(self)}")
  74. def _print_PythonMod(self, expr: sympy.Expr) -> str:
  75. raise NotImplementedError(f"_print_PythonMod not implemented for {type(self)}")
  76. def _print_IntTrueDiv(self, expr: sympy.Expr) -> str:
  77. raise NotImplementedError(f"_print_IntTrueDiv not implemented for {type(self)}")
  78. def _print_PowByNatural(self, expr: sympy.Expr) -> str:
  79. raise NotImplementedError(
  80. f"_print_PowByNatural not implemented for {type(self)}"
  81. )
  82. def _print_FloatPow(self, expr: sympy.Expr) -> str:
  83. raise NotImplementedError(f"_print_FloatPow not implemented for {type(self)}")
  84. def _print_TruncToInt(self, expr: sympy.Expr) -> str:
  85. raise NotImplementedError(f"_print_TruncToInt not implemented for {type(self)}")
  86. def _print_RoundToInt(self, expr: sympy.Expr) -> str:
  87. raise NotImplementedError(f"_print_RoundToInt not implemented for {type(self)}")
  88. def _print_RoundDecimal(self, expr: sympy.Expr) -> str:
  89. raise NotImplementedError(
  90. f"_print_RoundDecimal not implemented for {type(self)}"
  91. )
  92. # NB: Some float operations are INTENTIONALLY not implemented for
  93. # printers. You can implement them as a quick unblock, but it is better
  94. # to ask yourself why we haven't done this computation in the Tensor
  95. # universe instead
  96. def _print_TruncToFloat(self, expr: sympy.Expr) -> str:
  97. raise NotImplementedError(
  98. f"_print_TruncToFloat not implemented for {type(self)}"
  99. )
  100. class PythonPrinter(ExprPrinter):
  101. def _print_ToFloat(self, expr: sympy.Expr) -> str:
  102. assert len(expr.args) == 1
  103. # NB: We use sym_float here because the printer is used for cache
  104. # serialization, and cache guards get evaluated with SymInt to
  105. # propagate guards to the parent ShapeEnv. However, this comes at a
  106. # runtime cost for guards involving float. If this is unacceptable
  107. # overhead, what you want to do is have two separate printers for
  108. # SymInt, one for when the inputs are guaranteed to be int, and
  109. # another for when they could be SymInt.
  110. #
  111. # NB: sym_min/sym_max also have this problem, but I chose not to fix
  112. # those.
  113. #
  114. # See https://github.com/pytorch/pytorch/issues/142507 for more
  115. # context.
  116. return f"torch.sym_float({self._print(expr.args[0])})"
  117. def _print_And(self, expr: sympy.Expr) -> str:
  118. return self.stringify(expr.args, " and ", precedence(expr))
  119. def _print_Or(self, expr: sympy.Expr) -> str:
  120. return self.stringify(expr.args, " or ", precedence(expr))
  121. def _print_ModularIndexing(self, expr: sympy.Expr) -> str:
  122. x, div, mod = (
  123. self.parenthesize(arg, PRECEDENCE["Atom"] - 0.5) for arg in expr.args
  124. )
  125. if div != "1":
  126. x = f"({x} // {div})"
  127. return f"({x} % {mod})"
  128. def _print_Infinity(self, expr: sympy.Expr) -> str:
  129. return "math.inf"
  130. def _print_NegativeInfinity(self, expr: sympy.Expr) -> str:
  131. return "-math.inf"
  132. # WARNING: this is dangerous for Triton, which has C-style modulus
  133. def _print_PythonMod(self, expr: sympy.Expr) -> str:
  134. return self.stringify(expr.args, " % ", PRECEDENCE["Atom"] - 0.5)
  135. # WARNING: this is dangerous for Triton, which has C-style modulus
  136. def _print_FloorDiv(self, expr: sympy.Expr) -> str:
  137. x, div = (self.parenthesize(arg, PRECEDENCE["Atom"] - 0.5) for arg in expr.args)
  138. return f"{x} // {div}"
  139. # WARNING: this is dangerous for Triton, when lhs, rhs > 2**53, Python
  140. # does a special algorithm
  141. def _print_IntTrueDiv(self, expr: sympy.Expr) -> str:
  142. return self.stringify(expr.args, " / ", PRECEDENCE["Atom"] - 0.5)
  143. def _helper_sqrt(self, expr: sympy.Expr) -> str:
  144. return f"math.sqrt({self._print(expr)})"
  145. def _print_OpaqueUnaryFn_sqrt(self, expr: sympy.Expr) -> str:
  146. return self._helper_sqrt(expr.args[0])
  147. def _print_FloatPow(self, expr: sympy.Expr) -> str:
  148. return self.stringify(expr.args, " ** ", PRECEDENCE["Pow"])
  149. # TODO: Not sure this works with Triton, even when base/exp are integral
  150. def _print_PowByNatural(self, expr: sympy.Expr) -> str:
  151. return self.stringify(expr.args, " ** ", PRECEDENCE["Pow"])
  152. def _print_floor(self, expr: sympy.Expr) -> str:
  153. assert len(expr.args) == 1
  154. return f"math.floor({self._print(expr.args[0])})"
  155. def _print_FloorToInt(self, expr: sympy.Expr) -> str:
  156. assert len(expr.args) == 1
  157. return f"math.floor({self._print(expr.args[0])})"
  158. def _print_TruncToInt(self, expr: sympy.Expr) -> str:
  159. assert len(expr.args) == 1
  160. # This also could have been int(), they'll do the same thing for float
  161. return f"math.trunc({self._print(expr.args[0])})"
  162. def _print_ceiling(self, expr: sympy.Expr) -> str:
  163. assert len(expr.args) == 1
  164. return f"math.ceil({self._print(expr.args[0])})"
  165. def _print_CeilToInt(self, expr: sympy.Expr) -> str:
  166. assert len(expr.args) == 1
  167. return f"math.ceil({self._print(expr.args[0])})"
  168. def _print_Abs(self, expr: sympy.Expr) -> str:
  169. assert len(expr.args) == 1
  170. return f"abs({self._print(expr.args[0])})"
  171. # NB: It's expected that we've made explicit any promotion in the sympy
  172. # expression, so it doesn't matter that Python max/min doesn't perform
  173. # promotion
  174. def _print_Max(self, expr: sympy.Expr) -> str:
  175. assert len(expr.args) >= 2
  176. return f"max({', '.join(map(self._print, expr.args))})"
  177. def _print_Min(self, expr: sympy.Expr) -> str:
  178. assert len(expr.args) >= 2
  179. return f"min({', '.join(map(self._print, expr.args))})"
  180. def _print_OpaqueUnaryFn_cos(self, expr: sympy.Expr) -> str:
  181. assert len(expr.args) == 1
  182. return f"math.cos({self._print(expr.args[0])})"
  183. def _print_OpaqueUnaryFn_cosh(self, expr: sympy.Expr) -> str:
  184. assert len(expr.args) == 1
  185. return f"math.cosh({self._print(expr.args[0])})"
  186. def _print_OpaqueUnaryFn_acos(self, expr: sympy.Expr) -> str:
  187. assert len(expr.args) == 1
  188. return f"math.acos({self._print(expr.args[0])})"
  189. def _print_OpaqueUnaryFn_sin(self, expr: sympy.Expr) -> str:
  190. assert len(expr.args) == 1
  191. return f"math.sin({self._print(expr.args[0])})"
  192. def _print_OpaqueUnaryFn_sinh(self, expr: sympy.Expr) -> str:
  193. assert len(expr.args) == 1
  194. return f"math.sinh({self._print(expr.args[0])})"
  195. def _print_OpaqueUnaryFn_asin(self, expr: sympy.Expr) -> str:
  196. assert len(expr.args) == 1
  197. return f"math.asin({self._print(expr.args[0])})"
  198. def _print_OpaqueUnaryFn_tan(self, expr: sympy.Expr) -> str:
  199. assert len(expr.args) == 1
  200. return f"math.tan({self._print(expr.args[0])})"
  201. def _print_OpaqueUnaryFn_tanh(self, expr: sympy.Expr) -> str:
  202. assert len(expr.args) == 1
  203. return f"math.tanh({self._print(expr.args[0])})"
  204. def _print_OpaqueUnaryFn_atan(self, expr: sympy.Expr) -> str:
  205. assert len(expr.args) == 1
  206. return f"math.atan({self._print(expr.args[0])})"
  207. def _print_OpaqueUnaryFn_log2(self, expr: sympy.Expr) -> str:
  208. assert len(expr.args) == 1
  209. return f"math.log2({self._print(expr.args[0])})"
  210. def _print_RoundToInt(self, expr: sympy.Expr) -> str:
  211. assert len(expr.args) == 1
  212. return f"round({self._print(expr.args[0])})"
  213. def _print_RoundDecimal(self, expr: sympy.Expr) -> str:
  214. assert len(expr.args) == 2
  215. number, ndigits = expr.args
  216. assert isinstance(ndigits, sympy.Integer)
  217. return f"round({self._print(number)}, {ndigits})"
  218. class CppPrinter(ExprPrinter):
  219. def _print_Integer(self, expr: sympy.Expr) -> str:
  220. suffix = "LL" if sys.platform in ["darwin", "win32"] else "L"
  221. i = int(expr)
  222. if i > INDEX_TYPE_MAX or i < INDEX_TYPE_MIN:
  223. raise OverflowError(f"{i} too big to convert to {INDEX_TYPE}")
  224. elif i == INDEX_TYPE_MIN:
  225. assert i == (-1) << 63
  226. # Writing -9223372036854775808L makes the value overflow
  227. # as it is parsed as -(9223372036854775808L) by the C/C++ compiler
  228. return f"(-1{suffix} << 63)"
  229. return f"{i}{suffix}"
  230. def _print_Where(self, expr: sympy.Expr) -> str:
  231. c, p, q = (
  232. self.parenthesize(arg, PRECEDENCE["Atom"] - 0.5) for arg in expr.args
  233. )
  234. return f"{c} ? {p} : {q}"
  235. def _print_ModularIndexing(self, expr: sympy.Expr) -> str:
  236. x, div, mod = expr.args
  237. x = self.doprint(x)
  238. if div != 1:
  239. div = self.doprint(div)
  240. if expr.is_integer:
  241. x = f"c10::div_floor_integer(static_cast<int64_t>({x}), static_cast<int64_t>({div}))"
  242. else:
  243. x = f"c10::div_floor_floating(static_cast<double>({x}), static_cast<double>({div}))"
  244. mod = self.doprint(mod)
  245. return f"(static_cast<{INDEX_TYPE}>({x}) % static_cast<{INDEX_TYPE}>({mod}))"
  246. def _print_FloorDiv(self, expr: sympy.Expr) -> str:
  247. x, div = expr.args
  248. x = self.doprint(x)
  249. div = self.doprint(div)
  250. if expr.is_integer:
  251. return f"c10::div_floor_integer(static_cast<int64_t>({x}), static_cast<int64_t>({div}))"
  252. return f"c10::div_floor_floating(static_cast<double>({x}), static_cast<double>({div}))"
  253. def _print_floor(self, expr: sympy.Expr) -> str:
  254. assert len(expr.args) == 1
  255. r = f"std::floor({self._print(expr.args[0])})"
  256. return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
  257. def _print_FloorToInt(self, expr: sympy.Expr) -> str:
  258. assert len(expr.args) == 1
  259. r = f"std::floor({self._print(expr.args[0])})"
  260. return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
  261. def _print_TruncToInt(self, expr: sympy.Expr) -> str:
  262. assert len(expr.args) == 1
  263. r = f"std::trunc({self._print(expr.args[0])})"
  264. return f"static_cast<{INDEX_TYPE}>({r})"
  265. def _print_TruncToFloat(self, expr: sympy.Expr) -> str:
  266. assert len(expr.args) == 1
  267. return f"std::trunc({self._print(expr.args[0])})"
  268. def _print_ToFloat(self, expr: sympy.Expr) -> str:
  269. assert len(expr.args) == 1
  270. return f"static_cast<double>({self._print(expr.args[0])})"
  271. def _print_PythonMod(self, expr: sympy.Expr) -> str:
  272. x, div = expr.args
  273. x = self.doprint(x)
  274. div = self.doprint(div)
  275. return f"c10::div_mod({x}, {div})"
  276. def _print_IntTrueDiv(self, expr: sympy.Expr) -> str:
  277. lhs, rhs = expr.args
  278. # TODO: This is only accurate up to 2**53
  279. return f"static_cast<double>({self._print(lhs)}) / static_cast<double>({self._print(rhs)})"
  280. # TODO: PowByNatural: we need to implement our own int-int pow. Do NOT
  281. # use std::pow, that operates on floats
  282. def _print_PowByNatural(self, expr: sympy.Expr) -> str:
  283. # Implement the special-case of 2**x for now
  284. base, exp = expr.args
  285. if base == 2:
  286. return f"(1 << ({self._print(exp)}))"
  287. raise NotImplementedError(
  288. f"_print_PowByNatural not implemented for {type(self)}"
  289. )
  290. def _print_FloatPow(self, expr: sympy.Expr) -> str:
  291. base, exp = expr.args
  292. return f"std::pow({self._print(base)}, {self._print(exp)})"
  293. def _print_Pow(self, expr: sympy.Expr) -> str:
  294. # Uses float constants to perform FP div
  295. base, exp = expr.args
  296. if exp == 0.5 or exp == -0.5:
  297. base = self._print(base)
  298. return f"std::sqrt({base})" if exp == 0.5 else f"1.0/std::sqrt({base})"
  299. if exp.is_integer:
  300. exp = int(exp)
  301. if exp > 0:
  302. r = self.stringify([base] * exp, "*", PRECEDENCE["Mul"])
  303. elif exp < -1:
  304. r = (
  305. "1.0/("
  306. + self.stringify([base] * abs(exp), "*", PRECEDENCE["Mul"])
  307. + ")"
  308. )
  309. elif exp == -1:
  310. r = "1.0/" + self._print(base)
  311. else: # exp == 0
  312. r = "1.0"
  313. return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
  314. else:
  315. # TODO: float vs double
  316. return f"std::pow({base}, {float(exp)})"
  317. def _print_Rational(self, expr: sympy.Expr) -> str:
  318. # Uses float constants to perform FP div
  319. if expr.q == 1:
  320. r = f"{expr.p}"
  321. else:
  322. r = f"{expr.p}.0/{expr.q}.0"
  323. return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
  324. def _print_ceiling(self, expr: sympy.Expr) -> str:
  325. assert len(expr.args) == 1
  326. r = f"std::ceil({self._print(expr.args[0])})"
  327. return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
  328. def _print_CeilToInt(self, expr: sympy.Expr) -> str:
  329. assert len(expr.args) == 1
  330. r = f"std::ceil({self._print(expr.args[0])})"
  331. return f"static_cast<{INDEX_TYPE}>({r})" if expr.is_integer else r
  332. def _print_Min(self, expr: sympy.Expr) -> str:
  333. args = [self._print(a) for a in expr.args]
  334. if len(args) == 2:
  335. return f"std::min(static_cast<{INDEX_TYPE}>({args[0]}), static_cast<{INDEX_TYPE}>({args[1]}))"
  336. else:
  337. # Initializer list overload
  338. il = "{" + ", ".join(args) + "}"
  339. return f"std::min<{INDEX_TYPE}>({il})"
  340. def _print_Max(self, expr: sympy.Expr) -> str:
  341. args = [self._print(a) for a in expr.args]
  342. if len(args) == 2:
  343. return f"std::max(static_cast<{INDEX_TYPE}>({args[0]}), static_cast<{INDEX_TYPE}>({args[1]}))"
  344. else:
  345. # Initializer list overload
  346. il = "{" + ", ".join(args) + "}"
  347. return f"std::max<{INDEX_TYPE}>({il})"
  348. def _print_Abs(self, expr: sympy.Expr) -> str:
  349. assert len(expr.args) == 1
  350. return f"std::abs({self._print(expr.args[0])})"
  351. def _print_OpaqueUnaryFn_cos(self, expr: sympy.Expr) -> str:
  352. assert len(expr.args) == 1
  353. return f"std::cos({self._print(expr.args[0])})"
  354. def _print_OpaqueUnaryFn_cosh(self, expr: sympy.Expr) -> str:
  355. assert len(expr.args) == 1
  356. return f"std::cosh({self._print(expr.args[0])})"
  357. def _print_OpaqueUnaryFn_acos(self, expr: sympy.Expr) -> str:
  358. assert len(expr.args) == 1
  359. return f"std::acos({self._print(expr.args[0])})"
  360. def _print_OpaqueUnaryFn_sin(self, expr: sympy.Expr) -> str:
  361. assert len(expr.args) == 1
  362. return f"std::sin({self._print(expr.args[0])})"
  363. def _print_OpaqueUnaryFn_sinh(self, expr: sympy.Expr) -> str:
  364. assert len(expr.args) == 1
  365. return f"std::sinh({self._print(expr.args[0])})"
  366. def _print_OpaqueUnaryFn_asin(self, expr: sympy.Expr) -> str:
  367. assert len(expr.args) == 1
  368. return f"std::asin({self._print(expr.args[0])})"
  369. def _print_OpaqueUnaryFn_tan(self, expr: sympy.Expr) -> str:
  370. assert len(expr.args) == 1
  371. return f"std::tan({self._print(expr.args[0])})"
  372. def _print_OpaqueUnaryFn_tanh(self, expr: sympy.Expr) -> str:
  373. assert len(expr.args) == 1
  374. return f"std::tanh({self._print(expr.args[0])})"
  375. def _print_OpaqueUnaryFn_atan(self, expr: sympy.Expr) -> str:
  376. assert len(expr.args) == 1
  377. return f"std::atan({self._print(expr.args[0])})"
  378. def _print_OpaqueUnaryFn_sqrt(self, expr: sympy.Expr) -> str:
  379. return f"std::sqrt({self._print(expr.args[0])})"
  380. def _print_OpaqueUnaryFn_log2(self, expr: sympy.Expr) -> str:
  381. return f"std::log2({self._print(expr.args[0])})"
  382. def _print_RoundToInt(self, expr: sympy.Expr) -> str:
  383. assert len(expr.args) == 1
  384. # TODO: dispatch to llrint depending on index type
  385. return f"std::lrint({self._print(expr.args[0])})"
  386. def _print_RoundDecimal(self, expr: sympy.Expr) -> str:
  387. assert len(expr.args) == 2
  388. number, ndigits = expr.args
  389. if number.is_integer:
  390. # ndigits < 0 should have been filtered by the sympy function
  391. assert ndigits < 0
  392. raise ValueError(
  393. f"For integer inputs, only non-negative ndigits are currently supported, but got {ndigits}."
  394. )
  395. number_str = self.parenthesize(number, PRECEDENCE["Mul"])
  396. return f"static_cast<double>(std::nearbyint(1e{ndigits} * {number_str}) * 1e{-ndigits})"
  397. def _print_BooleanTrue(self, expr: sympy.Expr) -> str:
  398. return "true"
  399. def _print_BooleanFalse(self, expr: sympy.Expr) -> str:
  400. return "false"
  401. def _print_Infinity(self, expr: sympy.Expr) -> str:
  402. return "std::numeric_limits<double>::infinity()"
  403. def _print_NegativeInfinity(self, expr: sympy.Expr) -> str:
  404. return f"-{self._print_Infinity(expr)}"