Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
reaction_package.cpp
Go to the documentation of this file.
1// reaction_package.cpp — Luau のパッケージの反応表(分類 elements・species・reactions)を sim::ReactionTableDefinition に読む
2// (reaction_package.h。T-0021)。
3// 読むだけで、中身の検査(元素の釣り合い・知らない物質・係数・速度の範囲)はベイクに任せる。ここで落とすのは「形」の誤り
4// (欄の綴り・型・整数でない数・範囲の外)。どの値も Luau の倍精度から誤差なしで整数に直せるものだけ通す(04 R7)。
6
7#include <algorithm>
8#include <cmath>
9#include <format>
10#include <limits>
11#include <span>
12#include <utility>
13#include <vector>
14
15namespace bicameral::script {
16
17 namespace {
18
23 using sim::RuleTerm;
25
26 constexpr int64_t EXACT_INTEGER_LIMIT = int64_t{1} << 53; // 倍精度で誤差なしに表せる整数の上限
27 constexpr int64_t UINT32_LIMIT = std::numeric_limits<uint32_t>::max();
28 constexpr size_t DECIMAL_DIGITS_MAX = 19; // 10^19 − 1 < 2^64
29 constexpr int64_t DECIMAL_EXPONENT_MAX = 999;
30
31 // --- 欄を読む道具 ---
32
33 // 表の鍵が全部 allowed の中の文字列か(知らない欄は綴りの誤りとして落とす)
34 std::expected<void, std::string> CheckFields(const ScriptValue& table,
35 std::span<const std::string_view> allowed, std::string_view path) {
36 for (const ScriptField& field : table.fields) {
37 const bool known = field.key.IsString() &&
38 std::find(allowed.begin(), allowed.end(), field.key.text) != allowed.end();
39 if (!known)
40 return std::unexpected(std::format("{}: 知らない欄 {}", path, ToDebugText(field.key)));
41 }
42
43 return {};
44 }
45
46 std::expected<const ScriptValue*, std::string> RequireField(const ScriptValue& table, std::string_view field,
47 std::string_view path) {
48 const ScriptValue* value = table.Find(field);
49 if (value == nullptr)
50 return std::unexpected(std::format("{}: 欄 {} がない", path, field));
51
52 return value;
53 }
54
55 std::expected<void, std::string> RequireTable(const ScriptValue& value, std::string_view path) {
56 if (!value.IsTable())
57 return std::unexpected(std::format("{}: 表で書く", path));
58
59 return {};
60 }
61
62 // 整数で書いた数を読む(小数・±2^53 の外・[minimum, maximum] の外は誤り)
63 std::expected<int64_t, std::string> ReadInteger(const ScriptValue& value, std::string_view path,
64 int64_t minimum, int64_t maximum) {
65 constexpr auto LIMIT = static_cast<double>(EXACT_INTEGER_LIMIT);
66 const bool exact = value.IsNumber() && value.number >= -LIMIT && value.number <= LIMIT &&
67 std::trunc(value.number) == value.number;
68 if (!exact)
69 return std::unexpected(std::format("{}: 整数(±2^53 まで)で書く", path));
70
71 const auto integer = static_cast<int64_t>(value.number);
72 if (integer < minimum || integer > maximum)
73 return std::unexpected(std::format("{}: {} は範囲 {}〜{} の外", path, integer, minimum, maximum));
74
75 return integer;
76 }
77
78 std::expected<int64_t, std::string> ReadIntegerField(const ScriptValue& table, std::string_view field,
79 std::string_view path, int64_t minimum, int64_t maximum) {
80 const auto value = RequireField(table, field, path);
81 if (!value)
82 return std::unexpected(value.error());
83
84 return ReadInteger(**value, std::format("{}.{}", path, field), minimum, maximum);
85 }
86
87 // 表であることと、知らない欄が無いことを確かめる
88 std::expected<void, std::string> CheckRecord(const ScriptValue& value,
89 std::span<const std::string_view> allowed, std::string_view path) {
90 if (auto table = RequireTable(value, path); !table)
91 return table;
92
93 return CheckFields(value, allowed, path);
94 }
95
96 // --- 元素 ---
97
98 std::expected<ElementDefinition, std::string> ReadElement(const std::string& name, const ScriptValue& value,
99 const std::string& path) {
100 static constexpr std::string_view FIELDS[] = {"atomic_mass_mg_per_mol"};
101 if (auto record = CheckRecord(value, FIELDS, path); !record)
102 return std::unexpected(record.error());
103
104 const auto mass = ReadIntegerField(value, "atomic_mass_mg_per_mol", path, 1, UINT32_LIMIT);
105 if (!mass)
106 return std::unexpected(mass.error());
107
108 return ElementDefinition{.name = name, .atomicMass = static_cast<uint32_t>(*mass)};
109 }
110
111 // --- 物質 ---
112
113 std::expected<std::vector<ElementCount>, std::string> ReadComposition(const ScriptValue& value,
114 const std::string& path) {
115 if (auto table = RequireTable(value, path); !table)
116 return std::unexpected(table.error());
117
118 std::vector<ElementCount> composition;
119 for (const ScriptField& field : value.fields) {
120 if (!field.key.IsString())
121 return std::unexpected(std::format("{}: 鍵は元素の名前", path));
122
123 const auto count = ReadInteger(field.value, std::format("{}.{}", path, field.key.text), 1,
125 if (!count)
126 return std::unexpected(count.error());
127
128 composition.push_back({.element = field.key.text, .count = static_cast<uint32_t>(*count)});
129 }
130
131 return composition;
132 }
133
134 std::expected<SpeciesDefinition, std::string> ReadSpecies(const std::string& name, const ScriptValue& value,
135 const std::string& path) {
136 static constexpr std::string_view FIELDS[] = {"composition", "formation_enthalpy_j_per_mol",
137 "heat_capacity_mj_per_mol_k",
138 "thermal_conductivity_mw_per_m_k"};
139 if (auto record = CheckRecord(value, FIELDS, path); !record)
140 return std::unexpected(record.error());
141
142 const auto compositionValue = RequireField(value, "composition", path);
143 if (!compositionValue)
144 return std::unexpected(compositionValue.error());
145
146 auto composition = ReadComposition(**compositionValue, path + ".composition");
147 if (!composition)
148 return std::unexpected(composition.error());
149
150 const auto enthalpy = ReadIntegerField(value, "formation_enthalpy_j_per_mol", path, -EXACT_INTEGER_LIMIT,
152 const auto heatCapacity = ReadIntegerField(value, "heat_capacity_mj_per_mol_k", path, 1, UINT32_LIMIT);
153 const auto conductivity = ReadIntegerField(value, "thermal_conductivity_mw_per_m_k", path, 0, UINT32_LIMIT);
154 for (const auto* result : {&enthalpy, &heatCapacity, &conductivity}) {
155 if (!*result)
156 return std::unexpected(result->error());
157 }
158
159 return SpeciesDefinition{.name = name,
160 .composition = std::move(*composition),
161 .formationEnthalpy = *enthalpy,
162 .heatCapacity = static_cast<uint32_t>(*heatCapacity),
163 .thermalConductivity = static_cast<uint32_t>(*conductivity)};
164 }
165
166 // --- 規則 ---
167
168 // { 物質 = 係数 } を項の並びに。次数は ReadOrders が後で書く
169 std::expected<std::vector<RuleTerm>, std::string> ReadTerms(const ScriptValue& value, const std::string& path) {
170 if (auto table = RequireTable(value, path); !table)
171 return std::unexpected(table.error());
172
173 std::vector<RuleTerm> terms;
174 for (const ScriptField& field : value.fields) {
175 if (!field.key.IsString())
176 return std::unexpected(std::format("{}: 鍵は物質の名前", path));
177
178 const auto coefficient = ReadInteger(field.value, std::format("{}.{}", path, field.key.text), 1,
180 if (!coefficient)
181 return std::unexpected(coefficient.error());
182
183 terms.push_back({.species = field.key.text,
184 .coefficient = static_cast<uint32_t>(*coefficient),
185 .firstOrder = false});
186 }
187
188 return terms;
189 }
190
191 // orders = { 物質 = 0 か 1 }。省けば反応物は全部次数 1。書いたら、書かない反応物は次数 0
192 std::expected<void, std::string> ReadOrders(const ScriptValue* orders, std::vector<RuleTerm>& reactants,
193 const std::string& path) {
194 if (orders == nullptr) {
195 for (RuleTerm& term : reactants)
196 term.firstOrder = true;
197
198 return {};
199 }
200
201 if (auto table = RequireTable(*orders, path); !table)
202 return table;
203
204 for (const ScriptField& field : orders->fields) {
205 const auto term = std::find_if(reactants.begin(), reactants.end(), [&](const RuleTerm& reactant) {
206 return field.key.IsString() && reactant.species == field.key.text;
207 });
208 if (term == reactants.end())
209 return std::unexpected(std::format("{}: {} は反応物ではない", path, ToDebugText(field.key)));
210
211 const auto order = ReadInteger(field.value, std::format("{}.{}", path, field.key.text), 0, 1);
212 if (!order)
213 return std::unexpected(order.error());
214
215 term->firstOrder = *order == 1;
216 }
217
218 return {};
219 }
220
221 // A は 10 進の文字列か整数
222 std::expected<DecimalNumber, std::string> ReadPreExponential(const ScriptValue& value,
223 const std::string& path) {
224 if (value.IsString()) {
225 auto parsed = ParseDecimal(value.text);
226 if (!parsed)
227 return std::unexpected(std::format("{}: {}", path, parsed.error()));
228
229 return parsed;
230 }
231
232 const auto integer = ReadInteger(value, path, 1, EXACT_INTEGER_LIMIT);
233 if (!integer)
234 return std::unexpected(std::format("{}(10 進の文字列 \"2.8e19\" でもよい)", integer.error()));
235
236 return ParseDecimal(std::to_string(*integer));
237 }
238
239 std::expected<sim::ArrheniusRate, std::string> ReadRate(const ScriptValue& value, const std::string& path) {
240 static constexpr std::string_view FIELDS[] = {"a", "activation_energy_j_per_mol"};
241 if (auto record = CheckRecord(value, FIELDS, path); !record)
242 return std::unexpected(record.error());
243
244 const auto preExponentialValue = RequireField(value, "a", path);
245 if (!preExponentialValue)
246 return std::unexpected(preExponentialValue.error());
247
248 const auto preExponential = ReadPreExponential(**preExponentialValue, path + ".a");
249 if (!preExponential)
250 return std::unexpected(preExponential.error());
251
252 const auto activation = ReadIntegerField(value, "activation_energy_j_per_mol", path, 0,
254 if (!activation)
255 return std::unexpected(activation.error());
256
257 return sim::ArrheniusRate{.preExponentialMantissa = preExponential->mantissa,
258 .preExponentialExponent10 = preExponential->exponent10,
259 .activationEnergy = *activation};
260 }
261
262 // reactants・products を読む
263 std::expected<void, std::string> ReadRuleTerms(const ScriptValue& value, const std::string& path,
264 RuleDefinition& rule) {
265 for (const auto& [field, terms] : {std::pair{"reactants", &rule.reactants}, {"products", &rule.products}}) {
266 const auto termsValue = RequireField(value, field, path);
267 if (!termsValue)
268 return std::unexpected(termsValue.error());
269
270 auto read = ReadTerms(**termsValue, std::format("{}.{}", path, field));
271 if (!read)
272 return std::unexpected(read.error());
273
274 *terms = std::move(*read);
275 }
276
277 return ReadOrders(value.Find("orders"), rule.reactants, path + ".orders");
278 }
279
280 std::expected<RuleDefinition, std::string> ReadRule(const std::string& name, const ScriptValue& value,
281 const std::string& path) {
282 static constexpr std::string_view FIELDS[] = {"reactants", "products", "rate", "orders",
283 "reaction_enthalpy_j_per_mol"};
284 if (auto record = CheckRecord(value, FIELDS, path); !record)
285 return std::unexpected(record.error());
286
287 RuleDefinition rule{.name = name};
288 if (auto terms = ReadRuleTerms(value, path, rule); !terms)
289 return std::unexpected(terms.error());
290
291 const auto rateValue = RequireField(value, "rate", path);
292 if (!rateValue)
293 return std::unexpected(rateValue.error());
294
295 const auto rate = ReadRate(**rateValue, path + ".rate");
296 if (!rate)
297 return std::unexpected(rate.error());
298
299 rule.rate = *rate;
300 if (const ScriptValue* declared = value.Find("reaction_enthalpy_j_per_mol")) {
301 const auto enthalpy = ReadInteger(*declared, path + ".reaction_enthalpy_j_per_mol",
303 if (!enthalpy)
304 return std::unexpected(enthalpy.error());
305
306 rule.declaredReactionEnthalpy = *enthalpy;
307 }
308
309 return rule;
310 }
311
312 // --- 分類ごと ---
313
314 // 分類 category の鍵を全部 read で読み、out に足す(鍵はバイト順。ベイクも名前の順に並べ直す)
315 template <typename Definition, typename Reader>
316 std::expected<void, std::string> ReadCategory(const PackageSetResult& packages, std::string_view category,
317 Reader read, std::vector<Definition>& out) {
318 const auto found = packages.tables.find(std::string(category));
319 if (found == packages.tables.end())
320 return {};
321
322 for (const auto& [key, entry] : found->second) {
323 const std::string path = std::format("{}.{}", category, key);
324 auto definition = read(key, entry.value, path);
325 if (!definition)
326 return std::unexpected(std::format("パッケージ {}: {}", entry.package, definition.error()));
327
328 out.push_back(std::move(*definition));
329 }
330
331 return {};
332 }
333
334 // --- 10 進の数 ---
335
336 // 仮数の数字の並び(先頭と末尾の 0 を落とし、落とした末尾の数を指数へ)を数にする
337 std::expected<DecimalNumber, std::string> DigitsToDecimal(std::string digits, int64_t exponent10) {
338 const size_t first = digits.find_first_not_of('0');
339 if (first == std::string::npos)
340 return std::unexpected("0 より大きい数で書く");
341
342 digits.erase(0, first);
343 while (digits.back() == '0') {
344 digits.pop_back();
345 exponent10 += 1;
346 }
347
348 if (digits.size() > DECIMAL_DIGITS_MAX)
349 return std::unexpected(std::format("有効数字は {} 桁まで", DECIMAL_DIGITS_MAX));
350
351 if (exponent10 < -DECIMAL_EXPONENT_MAX || exponent10 > DECIMAL_EXPONENT_MAX)
352 return std::unexpected(std::format("指数は ±{} まで", DECIMAL_EXPONENT_MAX));
353
354 uint64_t mantissa = 0;
355 for (const char digit : digits)
356 mantissa = (mantissa * 10) + static_cast<uint64_t>(digit - '0');
357
358 return DecimalNumber{.mantissa = mantissa, .exponent10 = static_cast<int32_t>(exponent10)};
359 }
360
361 bool IsDigit(char character) {
362 return character >= '0' && character <= '9';
363 }
364
365 // "e" の後(符号と 1〜4 桁)を読む
366 std::expected<int64_t, std::string> ParseExponent(std::string_view text) {
367 const bool negative = !text.empty() && text.front() == '-';
368 if (!text.empty() && (text.front() == '-' || text.front() == '+'))
369 text.remove_prefix(1);
370
371 if (text.empty() || text.size() > 4 || !std::ranges::all_of(text, IsDigit))
372 return std::unexpected("指数の書き方の誤り");
373
374 int64_t written = 0;
375 for (const char digit : text)
376 written = (written * 10) + (digit - '0');
377
378 return negative ? -written : written;
379 }
380
381 } // namespace
382
383 std::expected<DecimalNumber, std::string> ParseDecimal(std::string_view text) {
384 const std::string error = std::format("10 進の数(例 \"2.8e19\")で書く: \"{}\"", text);
385 std::string digits;
386 int64_t exponent10 = 0;
387 size_t position = 0;
388 bool seenPoint = false;
389 for (; position < text.size() && text[position] != 'e' && text[position] != 'E'; ++position) {
390 const char character = text[position];
391 if (character == '.' && !seenPoint) {
392 seenPoint = true;
393 continue;
394 }
395
396 if (!IsDigit(character))
397 return std::unexpected(error);
398
399 digits.push_back(character);
400 exponent10 -= seenPoint ? 1 : 0;
401 }
402
403 if (digits.empty())
404 return std::unexpected(error);
405
406 if (position < text.size()) {
407 const auto written = ParseExponent(text.substr(position + 1));
408 if (!written)
409 return std::unexpected(error);
410
411 exponent10 += *written;
412 }
413
414 auto decimal = DigitsToDecimal(std::move(digits), exponent10);
415 if (!decimal)
416 return std::unexpected(std::format("{}: \"{}\"", decimal.error(), text));
417
418 return decimal;
419 }
420
421 std::expected<ReactionTableDefinition, std::string> ReadReactionTableDefinition(const PackageSetResult& packages) {
422 ReactionTableDefinition definition;
423 if (auto result = ReadCategory(packages, REACTION_ELEMENTS_CATEGORY, ReadElement, definition.elements); !result)
424 return std::unexpected(result.error());
425
426 if (auto result = ReadCategory(packages, REACTION_SPECIES_CATEGORY, ReadSpecies, definition.species); !result)
427 return std::unexpected(result.error());
428
429 if (auto result = ReadCategory(packages, REACTION_RULES_CATEGORY, ReadRule, definition.rules); !result)
430 return std::unexpected(result.error());
431
432 return definition;
433 }
434
435} // namespace bicameral::script
std::expected< int64_t, std::string > ParseExponent(std::string_view text)
std::expected< ElementDefinition, std::string > ReadElement(const std::string &name, const ScriptValue &value, const std::string &path)
std::expected< void, std::string > ReadRuleTerms(const ScriptValue &value, const std::string &path, RuleDefinition &rule)
std::expected< DecimalNumber, std::string > ReadPreExponential(const ScriptValue &value, const std::string &path)
std::expected< RuleDefinition, std::string > ReadRule(const std::string &name, const ScriptValue &value, const std::string &path)
std::expected< std::vector< RuleTerm >, std::string > ReadTerms(const ScriptValue &value, const std::string &path)
std::expected< void, std::string > CheckFields(const ScriptValue &table, std::span< const std::string_view > allowed, std::string_view path)
std::expected< void, std::string > RequireTable(const ScriptValue &value, std::string_view path)
std::expected< const ScriptValue *, std::string > RequireField(const ScriptValue &table, std::string_view field, std::string_view path)
std::expected< void, std::string > ReadOrders(const ScriptValue *orders, std::vector< RuleTerm > &reactants, const std::string &path)
std::expected< sim::ArrheniusRate, std::string > ReadRate(const ScriptValue &value, const std::string &path)
std::expected< void, std::string > CheckRecord(const ScriptValue &value, std::span< const std::string_view > allowed, std::string_view path)
std::expected< SpeciesDefinition, std::string > ReadSpecies(const std::string &name, const ScriptValue &value, const std::string &path)
std::expected< DecimalNumber, std::string > DigitsToDecimal(std::string digits, int64_t exponent10)
std::expected< void, std::string > ReadCategory(const PackageSetResult &packages, std::string_view category, Reader read, std::vector< Definition > &out)
std::expected< int64_t, std::string > ReadInteger(const ScriptValue &value, std::string_view path, int64_t minimum, int64_t maximum)
std::expected< int64_t, std::string > ReadIntegerField(const ScriptValue &table, std::string_view field, std::string_view path, int64_t minimum, int64_t maximum)
std::expected< std::vector< ElementCount >, std::string > ReadComposition(const ScriptValue &value, const std::string &path)
std::string ToDebugText(const ScriptValue &value)
std::expected< ReactionTableDefinition, std::string > ReadReactionTableDefinition(const PackageSetResult &packages)
std::expected< DecimalNumber, std::string > ParseDecimal(std::string_view text)
constexpr std::string_view REACTION_ELEMENTS_CATEGORY
constexpr std::string_view REACTION_SPECIES_CATEGORY
constexpr std::string_view REACTION_RULES_CATEGORY
std::map< std::string, std::map< std::string, TableEntry > > tables
std::vector< ScriptField > fields
const ScriptValue * Find(std::string_view key) const
std::vector< RuleTerm > reactants
std::vector< RuleTerm > products
std::optional< int64_t > declaredReactionEnthalpy