infotheory/spec/document/
mod.rs1#[cfg(feature = "tuner")]
4use super::core::CompiledCompressionBackend;
5use super::core::{CanonicalBytes, CompiledRateBackend};
6use super::{
7 SpecEnvironment, SpecError, SpecResult, compression_backend_to_json_value,
8 parse_compression_backend_json, parse_rate_backend_json, rate_backend_to_json_value,
9};
10use crate::aixi::common::ObservationKeyMode;
11use crate::aixi::common::resolve_random_seed;
12use crate::api::{CompressionBackend, RateBackend};
13use crate::spec::CanonicalJson;
14use std::fmt;
15use std::path::Path;
16use std::sync::Arc;
17
18pub const SPEC_DOCUMENT_SCHEMA_VERSION: u32 = 1;
20
21const DOCUMENT_MAGIC: &[u8; 4] = b"itsd";
22const DOCUMENT_BINARY_VERSION: u8 = 1;
23#[cfg(feature = "tuner")]
24const TUNE_CANONICALIZATION_CLASSIFICATION_VERSION: &str = "bounds-v1";
25
26#[cfg(feature = "tuner")]
27pub(crate) const CANDIDATE_EXTERNAL_ASSET_FORBIDDEN: &str = "candidate_external_asset_forbidden";
28
29#[cfg(feature = "tuner")]
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub(crate) enum TuneInvalidReason {
32 CandidateExternalAssetForbidden,
33 CandidateOutOfBounds,
34 CandidateCompileError,
35 InvalidActionIndex,
36 InapplicableAction,
37}
38
39#[cfg(feature = "tuner")]
40impl TuneInvalidReason {
41 pub(crate) const fn as_str(self) -> &'static str {
42 match self {
43 Self::CandidateExternalAssetForbidden => CANDIDATE_EXTERNAL_ASSET_FORBIDDEN,
44 Self::CandidateOutOfBounds => "candidate_out_of_bounds",
45 Self::CandidateCompileError => "candidate_compile_error",
46 Self::InvalidActionIndex => "invalid_action_index",
47 Self::InapplicableAction => "inapplicable_action",
48 }
49 }
50}
51
52mod binary;
53mod io;
54mod parser;
55mod pipeline;
56mod serializer;
57mod types;
58
59use binary::{builtin_environment_name, observation_key_mode_name};
60#[cfg(feature = "vm")]
61use binary::{
62 shared_memory_policy_name, vm_fuzz_mutator_name, vm_observation_policy_name,
63 vm_observation_stream_mode_name, vm_payload_encoding_name,
64};
65pub use io::load_spec_document;
66pub use types::*;
67
68impl ValidatedPlannerRunSpec {
69 pub fn canonical_spec(&self) -> &PlannerRunSpec {
71 self.canonical_spec.as_ref()
72 }
73
74 pub fn canonical_bytes(&self) -> &CanonicalBytes {
76 &self.canonical_bytes
77 }
78
79 pub fn compile(&self) -> SpecResult<CompiledPlannerRunSpec> {
81 pipeline::compile_validated_planner_run_spec(self)
82 }
83}
84
85#[cfg(feature = "tuner")]
86impl ValidatedTuneSpec {
87 pub fn canonical_spec(&self) -> &TuneSpec {
89 self.canonical_spec.as_ref()
90 }
91
92 pub fn canonical_bytes(&self) -> &CanonicalBytes {
94 &self.canonical_bytes
95 }
96
97 pub fn compile(&self) -> SpecResult<CompiledTuneSpec> {
99 pipeline::compile_validated_tune_spec(self)
100 }
101}
102
103impl ParsedSpecDocument {
104 pub fn document(&self) -> &SpecDocument {
106 &self.document
107 }
108
109 pub fn base_dir(&self) -> &Path {
111 self.base_dir.as_path()
112 }
113
114 pub fn into_document(self) -> SpecDocument {
116 self.document
117 }
118
119 pub fn validate(self) -> SpecResult<ValidatedSpecDocument> {
121 let env = SpecEnvironment::new(self.base_dir);
122 self.document.validate_in(&env)
123 }
124
125 pub fn compile(self) -> SpecResult<CompiledSpecDocument> {
127 self.validate()?.compile()
128 }
129}
130
131impl ValidatedSpecDocument {
132 pub fn canonical_bytes(&self) -> &CanonicalBytes {
134 match self {
135 Self::PlannerRun(validated) => validated.canonical_bytes(),
136 #[cfg(feature = "tuner")]
137 Self::Tune(validated) => validated.canonical_bytes(),
138 Self::RateBackend(validated) => validated.canonical_bytes(),
139 Self::CompressionBackend(validated) => validated.canonical_bytes(),
140 }
141 }
142
143 pub fn compile(&self) -> SpecResult<CompiledSpecDocument> {
145 match self {
146 Self::PlannerRun(validated) => {
147 Ok(CompiledSpecDocument::PlannerRun(validated.compile()?))
148 }
149 #[cfg(feature = "tuner")]
150 Self::Tune(validated) => Ok(CompiledSpecDocument::Tune(validated.compile()?)),
151 Self::RateBackend(validated) => {
152 Ok(CompiledSpecDocument::RateBackend(validated.compile()?))
153 }
154 Self::CompressionBackend(validated) => Ok(CompiledSpecDocument::CompressionBackend(
155 validated.compile()?,
156 )),
157 }
158 }
159}
160
161impl CompiledSpecDocument {
162 pub fn canonical_bytes(&self) -> &CanonicalBytes {
164 match self {
165 Self::PlannerRun(compiled) => compiled.canonical_bytes(),
166 #[cfg(feature = "tuner")]
167 Self::Tune(compiled) => compiled.canonical_bytes(),
168 Self::RateBackend(compiled) => compiled.canonical_bytes(),
169 Self::CompressionBackend(compiled) => compiled.canonical_bytes(),
170 }
171 }
172}
173
174impl PlannerRunSpec {
175 pub fn validate_in(&self, env: &SpecEnvironment) -> SpecResult<ValidatedPlannerRunSpec> {
177 let canonical = pipeline::canonicalize_planner_run(self, env)?;
178 Ok(ValidatedPlannerRunSpec {
179 canonical_bytes: CanonicalBytes::from(binary::encode_spec_document_payload(
180 &SpecDocument::PlannerRun(canonical.clone()),
181 )),
182 canonical_spec: Arc::new(canonical),
183 base_dir: env.base_dir().to_path_buf(),
184 })
185 }
186
187 pub fn validate(&self) -> SpecResult<ValidatedPlannerRunSpec> {
189 self.validate_in(&SpecEnvironment::default())
190 }
191
192 pub fn compile_in(&self, env: &SpecEnvironment) -> SpecResult<CompiledPlannerRunSpec> {
194 pipeline::compile_planner_run_spec(self, env.base_dir())
195 }
196
197 pub fn compile(&self) -> SpecResult<CompiledPlannerRunSpec> {
199 self.compile_in(&SpecEnvironment::default())
200 }
201}
202
203impl EnvironmentSpec {
204 pub fn validate_in(
206 &self,
207 assets: &[AssetBinding],
208 env: &SpecEnvironment,
209 ) -> SpecResult<EnvironmentSpec> {
210 pipeline::canonicalize_environment_spec(self, assets, env)
211 }
212
213 pub fn validate(&self, assets: &[AssetBinding]) -> SpecResult<EnvironmentSpec> {
215 self.validate_in(assets, &SpecEnvironment::default())
216 }
217
218 pub fn kind_str(&self) -> &'static str {
223 match self {
224 Self::Builtin { .. } => "builtin",
225 #[cfg(feature = "vm")]
226 Self::NyxVm(_) => "vm",
227 }
228 }
229}
230
231impl BuiltinEnvironmentSpec {
232 pub fn canonical_name(&self) -> &'static str {
237 match self {
238 Self::TunerBridge => "tuner_bridge",
239 Self::CoinFlip => "coin_flip",
240 Self::BiasedRockPaperScissor => "biased_rock_paper_scissor",
241 Self::KuhnPoker => "kuhn_poker",
242 Self::ExtendedTiger => "extended_tiger",
243 Self::TicTacToe => "tic_tac_toe",
244 Self::Blackjack => "blackjack",
245 Self::Platformer => "platformer",
246 }
247 }
248}
249
250#[cfg(feature = "tuner")]
251impl TuneSpec {
252 pub fn validate_in(&self, env: &SpecEnvironment) -> SpecResult<ValidatedTuneSpec> {
254 let canonical = pipeline::canonicalize_tune_spec(self, env)?;
255 Ok(ValidatedTuneSpec {
256 canonical_bytes: CanonicalBytes::from(binary::encode_spec_document_payload(
257 &SpecDocument::Tune(canonical.clone()),
258 )),
259 canonical_spec: Arc::new(canonical),
260 base_dir: env.base_dir().to_path_buf(),
261 })
262 }
263
264 pub fn validate(&self) -> SpecResult<ValidatedTuneSpec> {
266 self.validate_in(&SpecEnvironment::default())
267 }
268
269 pub fn compile_in(&self, env: &SpecEnvironment) -> SpecResult<CompiledTuneSpec> {
271 pipeline::compile_tune_spec(self, env.base_dir())
272 }
273
274 pub fn compile(&self) -> SpecResult<CompiledTuneSpec> {
276 self.compile_in(&SpecEnvironment::default())
277 }
278}
279
280impl SpecDocument {
281 pub fn to_binary(&self) -> Vec<u8> {
283 binary::encode_spec_document_payload(self)
284 }
285
286 pub fn parse_json_value(value: &serde_json::Value, base_dir: &Path) -> SpecResult<Self> {
288 parser::parse_spec_document_json_value(value, base_dir)
289 }
290
291 pub fn parse_json_value_staged(
293 value: &serde_json::Value,
294 base_dir: &Path,
295 ) -> SpecResult<ParsedSpecDocument> {
296 let document = Self::parse_json_value(value, base_dir)?;
297 Ok(ParsedSpecDocument {
298 document,
299 base_dir: base_dir.to_path_buf(),
300 })
301 }
302
303 pub fn from_binary(bytes: &[u8], base_dir: &Path) -> SpecResult<Self> {
305 binary::decode_spec_document(bytes, base_dir)
306 }
307
308 pub fn from_binary_staged(bytes: &[u8], base_dir: &Path) -> SpecResult<ParsedSpecDocument> {
310 let document = Self::from_binary(bytes, base_dir)?;
311 Ok(ParsedSpecDocument {
312 document,
313 base_dir: base_dir.to_path_buf(),
314 })
315 }
316
317 pub fn validate_in(&self, env: &SpecEnvironment) -> SpecResult<ValidatedSpecDocument> {
319 match self {
320 Self::PlannerRun(spec) => Ok(ValidatedSpecDocument::PlannerRun(spec.validate_in(env)?)),
321 #[cfg(feature = "tuner")]
322 Self::Tune(spec) => Ok(ValidatedSpecDocument::Tune(spec.validate_in(env)?)),
323 Self::RateBackend(backend) => Ok(ValidatedSpecDocument::RateBackend(
324 backend.validate_in(env)?,
325 )),
326 Self::CompressionBackend(backend) => Ok(ValidatedSpecDocument::CompressionBackend(
327 backend.validate_in(env)?,
328 )),
329 }
330 }
331
332 pub fn validate(&self) -> SpecResult<ValidatedSpecDocument> {
334 self.validate_in(&SpecEnvironment::default())
335 }
336
337 pub fn compile_in(&self, env: &SpecEnvironment) -> SpecResult<CompiledSpecDocument> {
339 self.validate_in(env)?.compile()
340 }
341
342 pub fn compile(&self) -> SpecResult<CompiledSpecDocument> {
344 self.compile_in(&SpecEnvironment::default())
345 }
346
347 pub fn kind_str(&self) -> &'static str {
349 match self {
350 Self::PlannerRun(_) => "planner_run",
351 #[cfg(feature = "tuner")]
352 Self::Tune(_) => "tune",
353 Self::RateBackend(_) => "rate_backend",
354 Self::CompressionBackend(_) => "compression_backend",
355 }
356 }
357}
358
359impl CanonicalJson for PlannerRunSpec {
360 fn to_canonical_json_value(&self) -> SpecResult<serde_json::Value> {
361 serializer::planner_run_to_json_value(self)
362 }
363}
364
365#[cfg(feature = "tuner")]
366impl CanonicalJson for TuneSpec {
367 fn to_canonical_json_value(&self) -> SpecResult<serde_json::Value> {
368 serializer::tune_spec_to_json_value(self)
369 }
370}
371
372impl CanonicalJson for SpecDocument {
373 fn to_canonical_json_value(&self) -> SpecResult<serde_json::Value> {
374 serializer::spec_document_to_json_value(self)
375 }
376}
377
378impl CompiledPlannerController {
379 pub fn predictor(&self) -> &CompiledRateBackend {
381 match self {
382 Self::McAixi { predictor, .. } => predictor,
383 Self::AiqiDiscounted { predictor, .. } => predictor,
384 #[cfg(feature = "aixi")]
385 Self::AiqiWarmstartExactJh { predictor, .. } => predictor,
386 }
387 }
388
389 pub fn kind_str(&self) -> &'static str {
391 match self {
392 Self::McAixi { .. } => "mc_aixi",
393 Self::AiqiDiscounted { .. } => "aiqi_discounted",
394 #[cfg(feature = "aixi")]
395 Self::AiqiWarmstartExactJh { .. } => "aiqi_warmstart_exact_jh",
396 }
397 }
398
399 pub fn backend_label(&self) -> String {
404 self.predictor().display_label()
405 }
406}
407
408impl CompiledPlannerRunSpec {
409 pub fn canonical_spec(&self) -> &PlannerRunSpec {
411 self.canonical_spec.as_ref()
412 }
413
414 pub fn canonical_bytes(&self) -> &CanonicalBytes {
416 &self.canonical_bytes
417 }
418
419 pub fn resolved_assets(&self) -> &[ResolvedAssetBinding] {
421 self.resolved_assets.as_ref()
422 }
423
424 pub fn interface(&self) -> &PlannerInterfaceSpec {
426 &self.interface
427 }
428
429 pub fn runtime(&self) -> &PlannerRuntimeSpec {
431 &self.runtime
432 }
433
434 pub fn resolved_random_seed(&self) -> u64 {
436 resolve_random_seed(self.runtime.random_seed)
437 }
438
439 pub fn controller(&self) -> &CompiledPlannerController {
441 &self.controller
442 }
443
444 pub fn action_bits(&self) -> usize {
446 self.action_bits
447 }
448}
449
450#[cfg(feature = "tuner")]
451impl CompiledTuneSpec {
452 pub fn canonical_spec(&self) -> &TuneSpec {
454 self.canonical_spec.as_ref()
455 }
456
457 pub fn canonical_bytes(&self) -> &CanonicalBytes {
459 &self.canonical_bytes
460 }
461
462 pub fn base_dir(&self) -> &Path {
464 self.base_dir.as_path()
465 }
466
467 pub fn resolved_assets(&self) -> &[ResolvedAssetBinding] {
469 self.resolved_assets.as_ref()
470 }
471
472 pub fn baseline_candidate(&self) -> &CompiledCompressionBackend {
474 &self.baseline_candidate
475 }
476
477 pub fn controller(&self) -> &CompiledTuneController {
479 &self.controller
480 }
481
482 pub fn candidate_canonicalization_version(&self) -> &'static str {
484 self.candidate_canonicalization_version
485 }
486
487 pub fn baseline_candidate_model_bytes(&self) -> usize {
489 self.baseline_candidate.canonical_bytes().len()
490 }
491}
492
493#[cfg(feature = "vm")]
494fn canonicalize_vm_observation_policy_name(name: &str) -> SpecResult<VmObservationPolicySpec> {
495 match name {
496 "from_guest" => Ok(VmObservationPolicySpec::FromGuest),
497 "output_hash" => Ok(VmObservationPolicySpec::OutputHash),
498 "raw_output" => Ok(VmObservationPolicySpec::RawOutput),
499 "shared_memory" => Ok(VmObservationPolicySpec::SharedMemory),
500 other => Err(SpecError::new(format!(
501 "unknown VM observation_policy '{other}'"
502 ))),
503 }
504}
505
506#[cfg(feature = "vm")]
507fn canonicalize_vm_observation_stream_mode_name(
508 name: &str,
509) -> SpecResult<VmObservationStreamModeSpec> {
510 match name {
511 "pad_truncate" => Ok(VmObservationStreamModeSpec::PadTruncate),
512 "pad" => Ok(VmObservationStreamModeSpec::Pad),
513 "truncate" => Ok(VmObservationStreamModeSpec::Truncate),
514 other => Err(SpecError::new(format!(
515 "unknown VM observation_stream_mode '{other}'"
516 ))),
517 }
518}
519
520#[cfg(feature = "vm")]
521fn canonicalize_vm_payload_encoding(
522 name: &str,
523 field_name: &str,
524) -> SpecResult<VmPayloadEncodingSpec> {
525 match name {
526 "utf8" => Ok(VmPayloadEncodingSpec::Utf8),
527 "hex" => Ok(VmPayloadEncodingSpec::Hex),
528 other => Err(SpecError::new(format!(
529 "unknown VM payload encoding '{other}' for {field_name}"
530 ))),
531 }
532}
533
534#[cfg(feature = "vm")]
535fn canonicalize_vm_fuzz_mutator_name(name: &str) -> SpecResult<VmFuzzMutatorSpec> {
536 match name {
537 "flip_bit" => Ok(VmFuzzMutatorSpec::FlipBit),
538 "flip_byte" => Ok(VmFuzzMutatorSpec::FlipByte),
539 "insert_byte" => Ok(VmFuzzMutatorSpec::InsertByte),
540 "delete_byte" => Ok(VmFuzzMutatorSpec::DeleteByte),
541 "splice_seed" => Ok(VmFuzzMutatorSpec::SpliceSeed),
542 "reset_seed" => Ok(VmFuzzMutatorSpec::ResetSeed),
543 "havoc" => Ok(VmFuzzMutatorSpec::Havoc),
544 other => Err(SpecError::new(format!("unknown VM fuzz mutator '{other}'"))),
545 }
546}
547
548impl fmt::Debug for ValidatedPlannerRunSpec {
549 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
550 f.debug_struct("ValidatedPlannerRunSpec")
551 .field("canonical_bytes_len", &self.canonical_bytes.len())
552 .finish()
553 }
554}
555
556#[cfg(feature = "tuner")]
557impl fmt::Debug for ValidatedTuneSpec {
558 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559 f.debug_struct("ValidatedTuneSpec")
560 .field("canonical_bytes_len", &self.canonical_bytes.len())
561 .finish()
562 }
563}
564
565#[cfg(test)]
566mod tests;