arxo-codegen 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. arxo_codegen/__init__.py +5 -0
  2. arxo_codegen/__main__.py +7 -0
  3. arxo_codegen/active-outcomes.json +6837 -0
  4. arxo_codegen/check.py +158 -0
  5. arxo_codegen/check_active_canaries.py +262 -0
  6. arxo_codegen/check_api.py +76 -0
  7. arxo_codegen/check_conformance.py +414 -0
  8. arxo_codegen/check_matrix.py +131 -0
  9. arxo_codegen/check_packages.py +281 -0
  10. arxo_codegen/check_semantic_probes.py +293 -0
  11. arxo_codegen/check_xlsx.py +320 -0
  12. arxo_codegen/consumer_runner.py +316 -0
  13. arxo_codegen/core_support.py +203 -0
  14. arxo_codegen/coverage.py +341 -0
  15. arxo_codegen/deliver.py +180 -0
  16. arxo_codegen/examples/xlsx/pricing.lawir.json +409 -0
  17. arxo_codegen/examples/xlsx/pricing.xlsx.json +52 -0
  18. arxo_codegen/excel_plan.py +602 -0
  19. arxo_codegen/harness/verify/ci/gates/backends/check_backends.py +1248 -0
  20. arxo_codegen/harness/verify/ci/gates/backends/l1_fixtures.py +463 -0
  21. arxo_codegen/inert-nodes.json +62 -0
  22. arxo_codegen/inert.py +91 -0
  23. arxo_codegen/inputs.py +59 -0
  24. arxo_codegen/package.py +565 -0
  25. arxo_codegen/plan.py +4053 -0
  26. arxo_codegen/recertify_registry.py +100 -0
  27. arxo_codegen/runtime-cert.json +5259 -0
  28. arxo_codegen/runtime_cert.py +233 -0
  29. arxo_codegen/survey.py +272 -0
  30. arxo_codegen/target_runner.py +101 -0
  31. arxo_codegen/targets/bsl/cert.bsl +113 -0
  32. arxo_codegen/targets/bsl/emit.py +356 -0
  33. arxo_codegen/targets/bsl/json.bsl +243 -0
  34. arxo_codegen/targets/bsl/runner.bsl +210 -0
  35. arxo_codegen/targets/bsl/runtime.bsl +858 -0
  36. arxo_codegen/targets/cs/Cert.cs +90 -0
  37. arxo_codegen/targets/cs/Runner.cs +154 -0
  38. arxo_codegen/targets/cs/Runtime.cs +243 -0
  39. arxo_codegen/targets/cs/emit.py +223 -0
  40. arxo_codegen/targets/dart/cert.dart +99 -0
  41. arxo_codegen/targets/dart/emit.py +206 -0
  42. arxo_codegen/targets/dart/json.dart +166 -0
  43. arxo_codegen/targets/dart/runner.dart +154 -0
  44. arxo_codegen/targets/dart/runtime.dart +293 -0
  45. arxo_codegen/targets/erl/cert.erl +64 -0
  46. arxo_codegen/targets/erl/emit.py +216 -0
  47. arxo_codegen/targets/erl/rt.erl +238 -0
  48. arxo_codegen/targets/erl/runner.erl +130 -0
  49. arxo_codegen/targets/ex/cert.exs +62 -0
  50. arxo_codegen/targets/ex/emit.py +204 -0
  51. arxo_codegen/targets/ex/runner.exs +124 -0
  52. arxo_codegen/targets/ex/runtime.ex +227 -0
  53. arxo_codegen/targets/go/apiBase.go +483 -0
  54. arxo_codegen/targets/go/apicheck.go +97 -0
  55. arxo_codegen/targets/go/cert.go +218 -0
  56. arxo_codegen/targets/go/emit.py +1806 -0
  57. arxo_codegen/targets/go/evaluate.go +1826 -0
  58. arxo_codegen/targets/go/runner.go +38 -0
  59. arxo_codegen/targets/go/runtime.go +2612 -0
  60. arxo_codegen/targets/java/Cert.java +91 -0
  61. arxo_codegen/targets/java/Json.java +148 -0
  62. arxo_codegen/targets/java/Runner.java +151 -0
  63. arxo_codegen/targets/java/Runtime.java +243 -0
  64. arxo_codegen/targets/java/emit.py +198 -0
  65. arxo_codegen/targets/kt/Cert.kt +67 -0
  66. arxo_codegen/targets/kt/Json.kt +103 -0
  67. arxo_codegen/targets/kt/Runner.kt +126 -0
  68. arxo_codegen/targets/kt/Runtime.kt +196 -0
  69. arxo_codegen/targets/kt/emit.py +195 -0
  70. arxo_codegen/targets/lua/cert.lua +82 -0
  71. arxo_codegen/targets/lua/emit.py +250 -0
  72. arxo_codegen/targets/lua/json.lua +133 -0
  73. arxo_codegen/targets/lua/runner.lua +167 -0
  74. arxo_codegen/targets/lua/runtime.lua +541 -0
  75. arxo_codegen/targets/php/cert.php +84 -0
  76. arxo_codegen/targets/php/emit.py +229 -0
  77. arxo_codegen/targets/php/json.php +144 -0
  78. arxo_codegen/targets/php/runner.php +189 -0
  79. arxo_codegen/targets/php/runtime.php +660 -0
  80. arxo_codegen/targets/py/api_base.py +334 -0
  81. arxo_codegen/targets/py/apicheck.py +51 -0
  82. arxo_codegen/targets/py/cert.py +140 -0
  83. arxo_codegen/targets/py/emit.py +1140 -0
  84. arxo_codegen/targets/py/evaluate.py +980 -0
  85. arxo_codegen/targets/py/runner.py +38 -0
  86. arxo_codegen/targets/py/runtime.py +1634 -0
  87. arxo_codegen/targets/rb/cert.rb +73 -0
  88. arxo_codegen/targets/rb/emit.py +203 -0
  89. arxo_codegen/targets/rb/runner.rb +130 -0
  90. arxo_codegen/targets/rb/runtime.rb +255 -0
  91. arxo_codegen/targets/rs/Cargo.lock +142 -0
  92. arxo_codegen/targets/rs/Cargo.toml +16 -0
  93. arxo_codegen/targets/rs/cert.rs +106 -0
  94. arxo_codegen/targets/rs/emit.py +233 -0
  95. arxo_codegen/targets/rs/prefetch.rs +1 -0
  96. arxo_codegen/targets/rs/runner.rs +181 -0
  97. arxo_codegen/targets/rs/runtime.rs +448 -0
  98. arxo_codegen/targets/scala/Cert.scala +77 -0
  99. arxo_codegen/targets/scala/Json.scala +140 -0
  100. arxo_codegen/targets/scala/Runner.scala +121 -0
  101. arxo_codegen/targets/scala/Runtime.scala +269 -0
  102. arxo_codegen/targets/scala/emit.py +193 -0
  103. arxo_codegen/targets/swift/cert.swift +105 -0
  104. arxo_codegen/targets/swift/emit.py +231 -0
  105. arxo_codegen/targets/swift/json.swift +208 -0
  106. arxo_codegen/targets/swift/runner.swift +133 -0
  107. arxo_codegen/targets/swift/runtime.swift +528 -0
  108. arxo_codegen/targets/ts/apiBase.ts +250 -0
  109. arxo_codegen/targets/ts/apicheck.ts +37 -0
  110. arxo_codegen/targets/ts/cert.ts +96 -0
  111. arxo_codegen/targets/ts/emit.py +1214 -0
  112. arxo_codegen/targets/ts/evaluate.ts +894 -0
  113. arxo_codegen/targets/ts/package-lock.json +45 -0
  114. arxo_codegen/targets/ts/package.json +9 -0
  115. arxo_codegen/targets/ts/runner.ts +31 -0
  116. arxo_codegen/targets/ts/runtime.ts +1369 -0
  117. arxo_codegen/targets/xlsx/README.ru.md +176 -0
  118. arxo_codegen/targets/xlsx/emit.py +198 -0
  119. arxo_codegen/temporal.py +59 -0
  120. arxo_codegen/test_alias_types.py +171 -0
  121. arxo_codegen/test_api_contract.py +89 -0
  122. arxo_codegen/test_calendar_policy.py +48 -0
  123. arxo_codegen/test_check.py +49 -0
  124. arxo_codegen/test_check_active_canaries.py +96 -0
  125. arxo_codegen/test_check_conformance.py +132 -0
  126. arxo_codegen/test_check_semantic_probes.py +85 -0
  127. arxo_codegen/test_check_xlsx.py +52 -0
  128. arxo_codegen/test_consumer_runner.py +175 -0
  129. arxo_codegen/test_core_support.py +80 -0
  130. arxo_codegen/test_e0212_decision_time.py +63 -0
  131. arxo_codegen/test_head_aggregate.py +71 -0
  132. arxo_codegen/test_inert.py +143 -0
  133. arxo_codegen/test_instant_types.py +113 -0
  134. arxo_codegen/test_key_conflict_scope.py +92 -0
  135. arxo_codegen/test_key_conflict_targets.py +17 -0
  136. arxo_codegen/test_package_layout.py +163 -0
  137. arxo_codegen/test_plan_language_version.py +44 -0
  138. arxo_codegen/test_positions_refusal.py +43 -0
  139. arxo_codegen/test_recertify_registry.py +77 -0
  140. arxo_codegen/test_ruby_target.py +41 -0
  141. arxo_codegen/test_runtime_cert.py +83 -0
  142. arxo_codegen/test_secondary_key_conflict_scope.py +17 -0
  143. arxo_codegen/test_stage_r.py +108 -0
  144. arxo_codegen/test_standalone_inputs.py +71 -0
  145. arxo_codegen/test_support.py +150 -0
  146. arxo_codegen/test_survey_inputs.py +115 -0
  147. arxo_codegen-0.4.0.dist-info/LICENSE +202 -0
  148. arxo_codegen-0.4.0.dist-info/METADATA +1673 -0
  149. arxo_codegen-0.4.0.dist-info/RECORD +152 -0
  150. arxo_codegen-0.4.0.dist-info/WHEEL +5 -0
  151. arxo_codegen-0.4.0.dist-info/entry_points.txt +13 -0
  152. arxo_codegen-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,121 @@
1
+ // Раннер Scala-бэкенда: тот же вход и тот же ответ, что у runner.rb.
2
+ //
3
+ // Протокол выхода общий для всех целей: ответ — JSON и код 0; названный отказ
4
+ // среза — JSON {"refused": текст} и код 3, текст побайтово одинаков между
5
+ // целями; любое другое исключение — крах, который ворота валят.
6
+ object Runner {
7
+ private val issues = scala.collection.mutable.ArrayBuffer.empty[String]
8
+ private var consts: Map[String, (String, V)] = Map.empty
9
+
10
+ def arg(a: J): V = a("kind").text match {
11
+ case "entity_ref" => Txt(a("id").text)
12
+ // §54: разыменование предшествует исполнению; член перечисления §38 либо
13
+ // имя без объявления остаётся идентичностью, как в ключе атома оракула.
14
+ case "const_ref" => consts.get(a("id").text).map(_._2).getOrElse(RT.sym(a("id").text))
15
+ case "value" =>
16
+ val v = a("value").text
17
+ a("type")("name").text match {
18
+ case "urn:law:std#Money" => RT.money(v, a("currency").text)
19
+ case "urn:law:std#Decimal" | "urn:law:std#Integer" => RT.dec(v)
20
+ case "urn:law:std#Text" => Txt(v)
21
+ case "urn:law:std#Date" => RT.date(v)
22
+ case t => throw new Refused(s"аргумент вне среза раннера: $t")
23
+ }
24
+ case kind => throw new Refused(s"аргумент вне среза раннера: $kind")
25
+ }
26
+
27
+ // Тип аргумента, если известен: у value — из узла, у const_ref — из объявления
28
+ // константы; entity_ref типа не несёт (как у оракула).
29
+ def argType(a: J): Option[String] = a("kind").text match {
30
+ case "value" => a("type")("name").str
31
+ case "const_ref" => consts.get(a("id").text).map(_._1)
32
+ case _ => None
33
+ }
34
+
35
+ // Совпадение типа с наследованием сущностей вверх от фактического.
36
+ def conforms(actual: String, expected: String): Boolean = {
37
+ val seen = scala.collection.mutable.Set.empty[String]
38
+ var cur: Option[String] = Some(actual)
39
+ while (cur.isDefined && !seen.contains(cur.get)) {
40
+ if (cur.get == expected) return true
41
+ seen += cur.get
42
+ cur = Model.BASES.get(cur.get)
43
+ }
44
+ false
45
+ }
46
+
47
+ // Арность и типы аргументов утверждения против сигнатуры §189 (E-0046).
48
+ def wellTyped(lit: J): Boolean = Model.PARAMS.get(lit("predicate").text) match {
49
+ case None => true
50
+ case Some(params) =>
51
+ val args = lit("args").items
52
+ val ok = args.length == params.length &&
53
+ params.zip(args).forall { case (expected, a) => argType(a).forall(conforms(_, expected)) }
54
+ if (!ok) issues += "TYPE_ERROR"
55
+ ok
56
+ }
57
+
58
+ // Ключ атома у оракула §208 включает ТИП значения: несогласованный запрос
59
+ // атома не находит — NEITHER без issue.
60
+ def queryTyped(lit: J): Boolean = Model.PARAMS.get(lit("predicate").text) match {
61
+ case None => false
62
+ case Some(params) =>
63
+ val args = lit("args").items
64
+ args.length == params.length &&
65
+ params.zip(args).forall { case (expected, a) => argType(a).forall(conforms(_, expected)) }
66
+ }
67
+
68
+ def keyConflictBlocks(predicate: String): Boolean = {
69
+ val tainted = scala.collection.mutable.Set.from(Model.keyConflictPredicates())
70
+ val waiting = scala.collection.mutable.Stack.from(tainted)
71
+ while (waiting.nonEmpty) {
72
+ val source = waiting.pop()
73
+ for (consumer <- Model.KEY_READERS.getOrElse(source, Vector.empty) if !tainted.contains(consumer)) {
74
+ tainted += consumer
75
+ waiting.push(consumer)
76
+ }
77
+ }
78
+ tainted.contains(predicate)
79
+ }
80
+
81
+ def run(argv: Array[String]): Unit = {
82
+ val kase = Json.read(argv(0))
83
+ val query = Json.read(argv(1))
84
+ RT.setLegalTime(kase("context")("legalTime").text)
85
+ consts = Model.consts()
86
+ Model.packageFacts()
87
+ for (a <- kase("assertions").items) {
88
+ val lit = a("literal")
89
+ if (lit("polarity").text != "positive") throw new Refused("§113: отрицательный факт вне L0-среза")
90
+ if (wellTyped(lit) && Model.PARAMS.contains(lit("predicate").text)) {
91
+ Model.register(lit("predicate").text, lit("args").items.map(arg))
92
+ }
93
+ }
94
+ Model.saturate()
95
+ for (_ <- 0 until Model.keyConflicts()) issues += "KEY_CONFLICT"
96
+ val kind = query("kind").text
97
+ if (kind != "truth") throw new Refused(s"вид запроса вне среза раннера: $kind")
98
+ val ql = query("literal")
99
+ val hit = queryTyped(ql) && {
100
+ val want = ql("args").items.map(arg)
101
+ Model.TABLES.get(ql("predicate").text).exists(_.exists { r =>
102
+ r.length == want.length && r.indices.forall(i => RT.eq(r(i), want(i)))
103
+ })
104
+ }
105
+ // Открытый мир: отсутствие строки — это NEITHER, а не FALSE_ONLY.
106
+ Json.out.println("{\"queryId\":" + Json.str(query("queryId").text) +
107
+ ",\"truthStatus\":" + Json.str(if (hit) "TRUE_ONLY" else "NEITHER") +
108
+ ",\"evaluationStatus\":" + Json.str(if (keyConflictBlocks(ql("predicate").text)) "NON_EXECUTABLE" else "COMPUTED") + ",\"issues\":[" + issues.map(Json.str).mkString(",") + "]}")
109
+ }
110
+
111
+ def main(argv: Array[String]): Unit = {
112
+ try run(argv)
113
+ catch {
114
+ case e: Refused =>
115
+ Json.out.println("{\"refused\":" + Json.str(e.getMessage) + "}")
116
+ Json.out.flush()
117
+ sys.exit(3)
118
+ }
119
+ Json.out.flush()
120
+ }
121
+ }
@@ -0,0 +1,269 @@
1
+ // Рантайм L0 для Scala. Тот же контракт, что у runtime.rb: точная десятичная
2
+ // арифметика §58 на BigInt, деньги с валютой, названный режим округления,
3
+ // календарный день, член перечисления.
4
+ //
5
+ // Ловушки Scala: `%` у BigInt усекает к нулю (евклидов остаток — `mod`);
6
+ // внутри `object RT` голый `max(...)` означает `RT.max`, поэтому целые
7
+ // сравниваются через `math.max`.
8
+
9
+ /** Названный отказ рантайма: то, что срез не считает, а не сбой. Раннер
10
+ * печатает его как JSON с кодом выхода 3; любое другое исключение — крах. */
11
+ final class Refused(msg: String) extends Exception(msg)
12
+
13
+ /** Значение ячейки. Text и сущность — `Txt`; календарный день §2.15 в
14
+ * канонической записи ISO — `LDate` (порядок записи и есть порядок дня);
15
+ * член перечисления §38 или ссылка без объявления — `Sym` (равенство по
16
+ * идентичности, порядок не определён, §57). */
17
+ sealed trait V
18
+ final case class Txt(v: String) extends V
19
+ final case class Dec(u: BigInt, s: Int) extends V
20
+ final case class Money(u: BigInt, s: Int, cur: String) extends V
21
+ final case class LDate(v: String) extends V
22
+ final case class Sym(id: String) extends V
23
+
24
+ object RT {
25
+ private def p10(k: Int): BigInt = BigInt(10).pow(k)
26
+
27
+ def dec(lit: String): V = {
28
+ val dot = lit.indexOf('.')
29
+ val i = if (dot < 0) lit else lit.substring(0, dot)
30
+ val f = if (dot < 0) "" else lit.substring(dot + 1)
31
+ val neg = i.startsWith("-")
32
+ val digits = (if (neg) i.substring(1) else i) + f
33
+ Dec(BigInt((if (neg) "-" else "") + digits), f.length)
34
+ }
35
+
36
+ def money(lit: String, cur: String): V = dec(lit) match {
37
+ case Dec(u, s) => Money(u, s, cur)
38
+ case other => other
39
+ }
40
+
41
+ def date(lit: String): V = LDate(lit)
42
+ def sym(id: String): V = Sym(id)
43
+
44
+ /** Модуль, масштаб и валюта числа; None — не число. */
45
+ private def num(v: V): Option[(BigInt, Int, Option[String])] = v match {
46
+ case Dec(u, s) => Some((u, s, None))
47
+ case Money(u, s, cur) => Some((u, s, Some(cur)))
48
+ case _ => None
49
+ }
50
+
51
+ private def make(u: BigInt, s: Int, cur: Option[String]): V = cur match {
52
+ case Some(c) => Money(u, s, c)
53
+ case None => Dec(u, s)
54
+ }
55
+
56
+ // §48/§58: сложение, вычитание, min/max требуют совпадения видов; у денег — и валюты.
57
+ private def same(a: V, b: V, op: String): ((BigInt, Int, Option[String]), (BigInt, Int, Option[String])) =
58
+ (num(a), num(b)) match {
59
+ case (Some(x), Some(y)) if x._3.isDefined == y._3.isDefined =>
60
+ if (x._3 != y._3) throw new Refused(s"§58: разные валюты: $op")
61
+ (x, y)
62
+ case _ => throw new Refused(s"§58: виды операндов не совпадают: $op")
63
+ }
64
+
65
+ private def pair(a: (BigInt, Int, Option[String]), b: (BigInt, Int, Option[String])): (BigInt, BigInt, Int) = {
66
+ val s = math.max(a._2, b._2)
67
+ (a._1 * p10(s - a._2), b._1 * p10(s - b._2), s)
68
+ }
69
+
70
+ // Целое значение: дробная часть нулевая (защита в глубину под статикой плана).
71
+ private def integral(v: V): Option[BigInt] = v match {
72
+ case Dec(u, s) if (u % p10(s)) == 0 => Some(u / p10(s))
73
+ case _ => None
74
+ }
75
+
76
+ def add(a: V, b: V): V = {
77
+ val (x, y) = same(a, b, "add")
78
+ val (p, q, s) = pair(x, y)
79
+ make(p + q, s, x._3)
80
+ }
81
+
82
+ def sub(a: V, b: V): V = {
83
+ val (x, y) = same(a, b, "sub")
84
+ val (p, q, s) = pair(x, y)
85
+ make(p - q, s, x._3)
86
+ }
87
+
88
+ // Правило скаляра §49: безразмерное масштабирует размерное; масштаб результата —
89
+ // сумма масштабов. Два размерных не перемножаются.
90
+ def mul(a: V, b: V): V = (num(a), num(b)) match {
91
+ case (Some(x), Some(y)) =>
92
+ if (x._3.isDefined && y._3.isDefined) throw new Refused("§58: произведение двух размерных величин не определено")
93
+ make(x._1 * y._1, x._2 + y._2, x._3.orElse(y._3))
94
+ case _ => throw new Refused("§58: виды операндов не совпадают: mul")
95
+ }
96
+
97
+ // Точное десятичное частное num/den при 10-гладком знаменателе (§18), иначе None.
98
+ private def exact(num0: BigInt, den0: BigInt): Option[(BigInt, Int)] = {
99
+ var g = num0.gcd(den0)
100
+ if (g == 0) g = BigInt(1)
101
+ var num = num0 / g
102
+ var den = den0 / g
103
+ if (den < 0) { num = -num; den = -den }
104
+ var twos = 0
105
+ var fives = 0
106
+ while (den % 2 == 0) { den /= 2; twos += 1 }
107
+ while (den % 5 == 0) { den /= 5; fives += 1 }
108
+ if (den != 1) None
109
+ else {
110
+ val k = math.max(twos, fives)
111
+ Some((num * BigInt(2).pow(k - twos) * BigInt(5).pow(k - fives), k))
112
+ }
113
+ }
114
+
115
+ // Таблица делимых §58 в срезе: Decimal/Decimal, Money/Decimal, Money/Money одной
116
+ // валюты. Integer/Integer даёт Rational — вне среза, отвергается планом статически.
117
+ def div(a: V, b: V): V = (num(a), num(b)) match {
118
+ case (Some(x), Some(y)) =>
119
+ if (y._1 == 0) throw new Refused("деление на ноль — evaluation error (§58)")
120
+ val like: Option[String] = (x._3, y._3) match {
121
+ case (Some(c), Some(d)) =>
122
+ if (c != d) throw new Refused("CURRENCY_MISMATCH: §58 делит Money на Money только при тождественной валюте")
123
+ None
124
+ case (Some(c), None) => Some(c)
125
+ case (None, Some(_)) => throw new Refused("§58: деление вне таблицы делимых")
126
+ case (None, None) => None
127
+ }
128
+ exact(x._1 * p10(y._2), y._1 * p10(x._2)) match {
129
+ case Some((qu, qs)) => make(qu, qs, like)
130
+ case None =>
131
+ if (like.isDefined) throw new Refused("INEXACT_DIVISION: округление только явной политикой (§50/§18)")
132
+ throw new Refused("§58: частное не 10-гладкое — Rational вне среза")
133
+ }
134
+ case _ => throw new Refused("§58: деление вне таблицы делимых")
135
+ }
136
+
137
+ // Евклидов остаток §58: 0 <= r < |b| при любых знаках операндов.
138
+ def mod(a: V, b: V): V = (integral(a), integral(b)) match {
139
+ case (Some(x), Some(y)) =>
140
+ if (y == 0) throw new Refused("остаток от деления на ноль — evaluation error (§58)")
141
+ Dec(x.mod(y.abs), 0)
142
+ case _ => throw new Refused("§58: mod определён только для Integer")
143
+ }
144
+
145
+ def min(a: V, b: V): V = {
146
+ val (x, y) = same(a, b, "min")
147
+ val (p, q, _) = pair(x, y)
148
+ if (p <= q) a else b
149
+ }
150
+
151
+ def max(a: V, b: V): V = {
152
+ val (x, y) = same(a, b, "max")
153
+ val (p, q, _) = pair(x, y)
154
+ if (p >= q) a else b
155
+ }
156
+
157
+ def abs(v: V): V = num(v) match {
158
+ case Some((u, s, cur)) => make(u.abs, s, cur)
159
+ case None => throw new Refused("§58: abs — не числовой вид")
160
+ }
161
+
162
+ private val PowMin = -(BigInt(1) << 63)
163
+ private val PowMax = (BigInt(1) << 63) - 1
164
+
165
+ // §58 (E-0075): только Integer pow Integer; порядок проверок — знак, предел,
166
+ // вычисление, диапазон — часть контракта.
167
+ def pow(base: V, exp: V): V = (integral(base), integral(exp)) match {
168
+ case (Some(b), Some(e)) =>
169
+ if (e < 0) throw new Refused("§58: pow — показатель отрицателен")
170
+ if (e > 4096) throw new Refused("RESOURCE_LIMIT: pow — показатель превышает 4096")
171
+ val r = b.pow(e.toInt)
172
+ if (r < PowMin || r > PowMax) throw new Refused("RESOURCE_LIMIT: pow — результат вне 64-битного диапазона")
173
+ Dec(r, 0)
174
+ case _ => throw new Refused("§58: pow определён только для Integer")
175
+ }
176
+
177
+ def round(m: V, digits: V, mode: String): V = {
178
+ if (mode != "HALF_UP") throw new Refused(s"§50: режим округления не реализован: $mode")
179
+ val (u, s, cur) = num(m).getOrElse(throw new Refused("§50: round — не числовой вид"))
180
+ val n = digits match {
181
+ case Dec(du, ds) => (du / p10(ds)).toInt
182
+ case _ => throw new Refused("§50: round — не числовой вид")
183
+ }
184
+ if (s <= n) make(u * p10(n - s), n, cur)
185
+ else {
186
+ val drop = p10(s - n)
187
+ val (q, r) = u.abs /% drop
188
+ val up = if (r * 2 >= drop) q + 1 else q // HALF_UP — от нуля
189
+ make(if (u < 0) -up else up, n, cur)
190
+ }
191
+ }
192
+
193
+ private def order(op: String, c: Int): Boolean = op match {
194
+ case "lt" => c < 0
195
+ case "le" => c <= 0
196
+ case "gt" => c > 0
197
+ case "ge" => c >= 0
198
+ case "eq" => c == 0
199
+ case "ne" => c != 0
200
+ case _ => throw new Refused(s"сравнение вне среза рантайма: $op")
201
+ }
202
+
203
+ /** Порядок записей по кодовым точкам: у записи ISO он и есть порядок дня. */
204
+ def strOrder(a: String, b: String): Int = Integer.signum(a.compareTo(b))
205
+
206
+ def cmp(a: V, op: String, b: V): Boolean = (a, b) match {
207
+ case (_: Sym, _) | (_, _: Sym) =>
208
+ val same = (a, b) match { case (Sym(x), Sym(y)) => x == y; case _ => false }
209
+ if (op == "eq") same
210
+ else if (op == "ne") !same
211
+ else throw new Refused("§57: вид enum не упорядочен")
212
+ case (LDate(x), LDate(y)) => order(op, strOrder(x, y))
213
+ case (_: LDate, _) | (_, _: LDate) =>
214
+ if (op == "eq") false
215
+ else if (op == "ne") true
216
+ else throw new Refused("§57: Date сравнивается только с Date")
217
+ case (_: Txt, _) | (_, _: Txt) =>
218
+ if (op == "eq") a == b
219
+ else if (op == "ne") a != b
220
+ else throw new Refused(s"порядок на Text вне среза рантайма: $op")
221
+ case _ =>
222
+ val x = num(a).get
223
+ val y = num(b).get
224
+ if (x._3.isDefined != y._3.isDefined) throw new Refused("§49: сравнение денег с безразмерным")
225
+ if (x._3 != y._3) throw new Refused("§49: разные валюты")
226
+ val (p, q, _) = pair(x, y)
227
+ order(op, p.compare(q))
228
+ }
229
+
230
+ def eq(a: V, b: V): Boolean = (a, b) match {
231
+ case (_: Sym, _) | (_, _: Sym) | (_: LDate, _) | (_, _: LDate) | (_: Txt, _) | (_, _: Txt) => a == b
232
+ case _ =>
233
+ val x = num(a).get
234
+ val y = num(b).get
235
+ if (x._3 != y._3) false
236
+ else {
237
+ val (p, q, _) = pair(x, y)
238
+ p == q
239
+ }
240
+ }
241
+
242
+ private def norm(u0: BigInt, s0: Int): String = {
243
+ var u = u0
244
+ var s = s0
245
+ while (s > 0 && u % 10 == 0) { u /= 10; s -= 1 }
246
+ s"${u}e-$s"
247
+ }
248
+
249
+ // Разделитель и экран внутри текста экранируются: иначе ["a|e:b","c"] и
250
+ // ["a","b|e:c"] склеивались в один ключ, и второй факт терялся.
251
+ private def esc(text: String): String = text.replace("\\", "\\\\").replace("|", "\\|")
252
+
253
+ def key(row: Seq[V]): String = row.map {
254
+ case Txt(t) => "e:" + esc(t)
255
+ case LDate(d) => "t:" + d
256
+ case Sym(id) => "c:" + esc(id)
257
+ case Money(u, s, cur) => "m:" + cur + ":" + norm(u, s)
258
+ case Dec(u, s) => "d:" + norm(u, s)
259
+ }.mkString("|")
260
+
261
+ private var legal = ""
262
+
263
+ def setLegalTime(t: String): Unit = legal = t
264
+
265
+ def legalTime(): String = {
266
+ if (legal.isEmpty) throw new Refused("legalTime не подан делом")
267
+ legal
268
+ }
269
+ }
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env python3
2
+ """Эмиттер Scala. Тот же план и тот же контракт рантайма; модель — `object
3
+ Model` с изменяемыми таблицами, отказ — исключение `Refused`."""
4
+ import json
5
+ import shutil
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ # Версия плана, которую этот эмиттер умеет печатать. Другая версия — отказ:
10
+ # незнакомое поле плана печаталось бы молча, а это другой ответ, не меньший.
11
+ SUPPORTED_PLAN = "law-backend-plan/0.7"
12
+ MUT = "scala.collection.mutable"
13
+
14
+
15
+ def check_plan(plan: dict) -> None:
16
+ v = plan.get("planVersion")
17
+ if v != SUPPORTED_PLAN:
18
+ raise ValueError(f"версия плана не поддержана эмиттером: {v!r} (поддержана {SUPPORTED_PLAN})")
19
+
20
+
21
+ def ss(s: str) -> str:
22
+ """Строковый литерал Scala: экраны JSON (`\\uXXXX`, `\\n`, `\\"`) Scala
23
+ понимает, `$` в обычной строке не интерполируется."""
24
+ return json.dumps(s, ensure_ascii=False)
25
+
26
+
27
+ def term(t: dict) -> str:
28
+ k = t["k"]
29
+ if k == "var":
30
+ return f'r{t["row"]}({t["col"]})'
31
+ if k == "lit":
32
+ if t["t"] == "money":
33
+ return f'RT.money({ss(t["v"])}, {ss(t["cur"])})'
34
+ if t["t"] == "dec":
35
+ return f'RT.dec({ss(t["v"])})'
36
+ if t["t"] == "date":
37
+ return f'RT.date({ss(t["v"])})'
38
+ if t["t"] == "enum":
39
+ return f'RT.sym({ss(t["v"])})'
40
+ return f'Txt({ss(t["v"])})'
41
+ if k == "bin":
42
+ return f'RT.{t["op"]}({term(t["l"])}, {term(t["r"])})'
43
+ if k == "call":
44
+ args = t["args"]
45
+ if t["fn"] == "round":
46
+ return f'RT.round({term(args[0])}, {term(args[1])}, {ss(args[2]["v"])})'
47
+ return f'RT.{t["fn"]}(' + ", ".join(term(a) for a in args) + ')'
48
+ raise AssertionError(k)
49
+
50
+
51
+ def listing(items: list, indent: str) -> str:
52
+ """Перечень аргументов по строке на элемент, без висячей запятой."""
53
+ return ("\n" + ",\n".join(indent + x for x in items) + "\n" + indent[:-2]) if items else ""
54
+
55
+
56
+ def emit(plan: dict) -> str:
57
+ check_plan(plan)
58
+ L = ["object Model {"]
59
+ L.append(f" val TABLES: {MUT}.LinkedHashMap[String, {MUT}.ArrayBuffer[Vector[V]]] = {MUT}.LinkedHashMap(" +
60
+ listing([f'{ss(t["pred"])} -> {MUT}.ArrayBuffer.empty[Vector[V]]' for t in plan["tables"]], " ") + ")")
61
+ L.append(" // Объявленные типы параметров: раннер сверяет с ними аргументы дела (§189).")
62
+ L.append(" val PARAMS: Map[String, Vector[String]] = Map(" +
63
+ listing([f'{ss(t["pred"])} -> Vector(' + ", ".join(ss(p) for p in t["params"]) + ")"
64
+ for t in plan["tables"]], " ") + ")")
65
+ L.append(" // Константы §54: тип для проверки §189 и значение для разыменования аргументов дела.")
66
+ L.append(" def consts(): Map[String, (String, V)] = Map(" +
67
+ # Двойные скобки: `a -> (b, c)` Scala 3 читает как два аргумента `->`.
68
+ listing([f'{ss(cid)} -> (({ss(c["type"])}, {term(c["term"])}))' for cid, c in plan["consts"].items()],
69
+ " ") + ")")
70
+ L.append(" // Наследование сущностей: подтип подходит параметру базового типа.")
71
+ L.append(" val BASES: Map[String, String] = Map(" +
72
+ listing([f"{ss(sub)} -> {ss(base)}" for sub, base in plan["bases"].items()], " ") + ")")
73
+ L.append(" // Ключи отношений §45.1: позиции столбцов каждого ключа.")
74
+ L.append(" val KEYS: Vector[(String, Vector[Vector[Int]])] = Vector(" +
75
+ listing([f'({ss(t["pred"])}, Vector(' + ", ".join("Vector(" + ", ".join(map(str, k)) + ")" for k in t["keys"]) + "))"
76
+ for t in plan["tables"] if t["keys"]], " ") + ")")
77
+ readers: dict[str, set[str]] = {}
78
+ for rule in plan["rules"]:
79
+ for atom in rule.get("atoms") or []:
80
+ readers.setdefault(atom["pred"], set()).add(rule["head"]["pred"])
81
+ for check in rule.get("checks") or []:
82
+ readers.setdefault(check["pred"], set()).add(rule["head"]["pred"])
83
+ L.append(" // E-0210/E-0242: forward readers of a keyed relation.")
84
+ L.append(" val KEY_READERS: Map[String, Vector[String]] = Map(" +
85
+ listing([f'{ss(source)} -> Vector(' + ", ".join(ss(value) for value in sorted(consumers)) + ")"
86
+ for source, consumers in sorted(readers.items())], " ") + ")")
87
+ L.append(f" private val seen = {MUT}.Map.empty[String, {MUT}.Set[String]]")
88
+ L.append("")
89
+ L.append(" private def addRow(urn: String, row: Vector[V]): Boolean =")
90
+ L.append(f" if (!seen.getOrElseUpdate(urn, {MUT}.Set.empty[String]).add(RT.key(row))) false")
91
+ L.append(" else { TABLES(urn) += row; true }")
92
+ L.append("")
93
+ for r in plan["rules"]:
94
+ L.append(f' // {r["id"]}')
95
+ L.append(f' private def rule_{r["name"]}(): Boolean = {{')
96
+ L.append(" var grew = false")
97
+ if r["window"]:
98
+ if r["window"].get("start"):
99
+ L.append(f' if (RT.strOrder(RT.legalTime(), {ss(r["window"]["start"])}) < 0) return false')
100
+ if r["window"].get("end"):
101
+ L.append(f' if (RT.strOrder(RT.legalTime(), {ss(r["window"]["end"])}) >= 0) return false')
102
+ ind = " "
103
+ for i, a in enumerate(r["atoms"]):
104
+ # `toVector` — снимок таблицы на входе в цикл.
105
+ L.append(f'{ind}for (r{i} <- TABLES({ss(a["pred"])}).toVector) {{')
106
+ ind += " "
107
+ conds = []
108
+ for (ai, aj, _at), (bi, bj, _bt) in r["joins"]:
109
+ conds.append(f'RT.cmp(r{ai}({aj}), "eq", r{bi}({bj}))')
110
+ for f in r["filters"]:
111
+ conds.append(f'RT.eq(r{f["row"]}({f["col"]}), {term(f["term"])})')
112
+ for g in r["guards"]:
113
+ conds.append(f'RT.cmp({term(g["l"])}, "{g["op"]}", {term(g["r"])})')
114
+ args = ", ".join(term(a) for a in r["head"]["args"])
115
+ add = f'if (addRow({ss(r["head"]["pred"])}, Vector[V]({args}))) grew = true'
116
+ if conds:
117
+ # `&&` вычисляется по порядку и обрывается на первом ложном — тот же
118
+ # порядок отсева и тех же отказов, что у `next unless` в Ruby.
119
+ L.append(f"{ind}if (" + " && ".join(conds) + ") {")
120
+ L.append(f"{ind} {add}")
121
+ L.append(f"{ind}}}")
122
+ else:
123
+ L.append(f"{ind}{add}")
124
+ for _ in r["atoms"]:
125
+ ind = ind[:-2]
126
+ L.append(ind + "}")
127
+ L.append(" grew")
128
+ L.append(" }")
129
+ L.append("")
130
+ L.append(" def saturate(): Unit = {")
131
+ L.append(" var grew = true")
132
+ L.append(" while (grew) {")
133
+ L.append(" grew = false")
134
+ for r in plan["rules"]:
135
+ L.append(f' if (rule_{r["name"]}()) grew = true')
136
+ L.append(" }")
137
+ L.append(" }")
138
+ L.append("")
139
+ L.append(" def register(urn: String, row: Vector[V]): Boolean = addRow(urn, row)")
140
+ L.append("")
141
+ L.append(" // KEY_CONFLICT §45.1/§196: группы строк одной таблицы с одинаковой проекцией на ключ; строки уникальны по ключу строки, поэтому группа из двух и более строк — различные accepted-кортежи. Считается после насыщения.")
142
+ L.append(" def keyConflicts(): Int = KEYS.map { case (urn, keys) =>")
143
+ L.append(" keys.map(key => TABLES(urn).groupBy(row => RT.key(key.map(row(_)))).count(_._2.size > 1)).sum")
144
+ L.append(" }.sum")
145
+ L.append("")
146
+ L.append(" def keyConflictPredicates(): Set[String] = KEYS.flatMap { case (urn, keys) =>")
147
+ L.append(" if (keys.exists(key => TABLES(urn).groupBy(row => RT.key(key.map(row(_)))).exists(_._2.size > 1))) Some(urn) else None")
148
+ L.append(" }.toSet")
149
+ L.append("")
150
+ L.append(" // Собственные факты пакета: раннер кладёт их до фактов дела.")
151
+ L.append(" def packageFacts(): Unit = {")
152
+ for f in plan["facts"]:
153
+ L.append(f' addRow({ss(f["pred"])}, Vector[V](' + ", ".join(term(a) for a in f["args"]) + "))")
154
+ L.append(" }")
155
+ L.append("}")
156
+ return "\n".join(L) + "\n"
157
+
158
+
159
+ # --- дескриптор цели ---------------------------------------------------------
160
+ HERE = Path(__file__).resolve().parent
161
+ EXT = "scala"
162
+ MODEL = "Model.scala"
163
+ MAINS = {"runner": "Runner.scala", "cert": "Cert.scala"}
164
+
165
+
166
+ def available():
167
+ cli = shutil.which("scala-cli")
168
+ return (True, cli) if cli else (False, "нет scala-cli в PATH")
169
+
170
+
171
+ def layout(proj, main: str) -> list:
172
+ proj = Path(proj)
173
+ proj.mkdir(parents=True, exist_ok=True)
174
+ out = []
175
+ for name in ("Runtime.scala", "Json.scala", MAINS[main]):
176
+ shutil.copyfile(HERE / name, proj / name)
177
+ out.append(proj / name)
178
+ return out
179
+
180
+
181
+ def build(proj):
182
+ return ["scala-cli", "compile", "-q", str(proj)], None
183
+
184
+
185
+ def run(proj, main: str):
186
+ return ["scala-cli", "run", "-q", str(proj), "--"], None
187
+
188
+
189
+ if __name__ == "__main__":
190
+ sys.path.insert(0, str(HERE.parents[1]))
191
+ import plan as P
192
+ ir = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
193
+ Path(sys.argv[2]).write_text(emit(P.build_plan(ir)), encoding="utf-8")