1use crate::ast::*;
2use std::collections::HashSet;
3
4#[derive(Debug, Clone)]
5pub struct RustOutput {
6 pub cargo_toml: String,
7 pub lib_rs: String,
8 pub types_rs: String,
9 pub entity_rs: String,
10}
11
12impl RustOutput {
13 pub fn full_lib(&self) -> String {
14 format!(
15 "{}\n\n// types.rs\n{}\n\n// entity.rs\n{}",
16 self.lib_rs, self.types_rs, self.entity_rs
17 )
18 }
19
20 pub fn mod_rs(&self) -> String {
21 self.lib_rs.clone()
22 }
23}
24
25#[derive(Debug, Clone)]
26pub struct RustConfig {
27 pub crate_name: String,
28 pub sdk_version: String,
29 pub module_mode: bool,
30 pub url: Option<String>,
32}
33
34impl Default for RustConfig {
35 fn default() -> Self {
36 Self {
37 crate_name: "generated-stack".to_string(),
38 sdk_version: "0.2".to_string(),
39 module_mode: false,
40 url: None,
41 }
42 }
43}
44
45pub fn compile_serializable_spec(
46 spec: SerializableStreamSpec,
47 entity_name: String,
48 config: Option<RustConfig>,
49) -> Result<RustOutput, String> {
50 let config = config.unwrap_or_default();
51 let compiler = RustCompiler::new(spec, entity_name, config);
52 Ok(compiler.compile())
53}
54
55pub fn write_rust_crate(
56 output: &RustOutput,
57 crate_dir: &std::path::Path,
58) -> Result<(), std::io::Error> {
59 std::fs::create_dir_all(crate_dir.join("src"))?;
60 std::fs::write(crate_dir.join("Cargo.toml"), &output.cargo_toml)?;
61 std::fs::write(crate_dir.join("src/lib.rs"), &output.lib_rs)?;
62 std::fs::write(crate_dir.join("src/types.rs"), &output.types_rs)?;
63 std::fs::write(crate_dir.join("src/entity.rs"), &output.entity_rs)?;
64 Ok(())
65}
66
67pub fn write_rust_module(
68 output: &RustOutput,
69 module_dir: &std::path::Path,
70) -> Result<(), std::io::Error> {
71 std::fs::create_dir_all(module_dir)?;
72 std::fs::write(module_dir.join("mod.rs"), output.mod_rs())?;
73 std::fs::write(module_dir.join("types.rs"), &output.types_rs)?;
74 std::fs::write(module_dir.join("entity.rs"), &output.entity_rs)?;
75 Ok(())
76}
77
78pub(crate) struct RustCompiler {
79 spec: SerializableStreamSpec,
80 entity_name: String,
81 config: RustConfig,
82}
83
84impl RustCompiler {
85 pub(crate) fn new(
86 spec: SerializableStreamSpec,
87 entity_name: String,
88 config: RustConfig,
89 ) -> Self {
90 Self {
91 spec,
92 entity_name,
93 config,
94 }
95 }
96
97 fn compile(&self) -> RustOutput {
98 RustOutput {
99 cargo_toml: self.generate_cargo_toml(),
100 lib_rs: self.generate_lib_rs(),
101 types_rs: self.generate_types_rs(),
102 entity_rs: self.generate_entity_rs(),
103 }
104 }
105
106 fn generate_cargo_toml(&self) -> String {
107 format!(
108 r#"[package]
109name = "{}"
110version = "0.1.0"
111edition = "2021"
112
113[dependencies]
114hyperstack-sdk = "{}"
115serde = {{ version = "1", features = ["derive"] }}
116serde_json = "1"
117"#,
118 self.config.crate_name, self.config.sdk_version
119 )
120 }
121
122 fn generate_lib_rs(&self) -> String {
123 let stack_name = self.derive_stack_name();
124 let entity_name = &self.entity_name;
125
126 format!(
127 r#"mod entity;
128mod types;
129
130pub use entity::{{{stack_name}Stack, {stack_name}StackViews, {entity_name}EntityViews}};
131pub use types::*;
132
133pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}};
134"#,
135 stack_name = stack_name,
136 entity_name = entity_name
137 )
138 }
139
140 fn generate_types_rs(&self) -> String {
141 let mut output = String::new();
142 output.push_str("use serde::{Deserialize, Serialize};\n\n");
143
144 let mut generated = HashSet::new();
145
146 for section in &self.spec.sections {
147 if !Self::is_root_section(§ion.name)
148 && section.fields.iter().any(|field| field.emit)
149 && generated.insert(section.name.clone())
150 {
151 output.push_str(&self.generate_struct_for_section(section));
152 output.push_str("\n\n");
153 }
154 }
155
156 output.push_str(&self.generate_main_entity_struct());
157 output.push_str(&self.generate_resolved_types(&mut generated));
158 output.push_str(&self.generate_event_wrapper());
159
160 output
161 }
162
163 pub(crate) fn generate_struct_for_section(&self, section: &EntitySection) -> String {
164 let struct_name = format!("{}{}", self.entity_name, to_pascal_case(§ion.name));
165 let mut fields = Vec::new();
166
167 for field in §ion.fields {
168 if !field.emit {
169 continue;
170 }
171 let field_name = to_snake_case(&field.field_name);
172 let rust_type = self.field_type_to_rust(field);
173
174 fields.push(format!(
175 " #[serde(default)]\n pub {}: {},",
176 field_name, rust_type
177 ));
178 }
179
180 format!(
181 "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
182 struct_name,
183 fields.join("\n")
184 )
185 }
186
187 pub(crate) fn is_root_section(name: &str) -> bool {
188 name.eq_ignore_ascii_case("root")
189 }
190
191 pub(crate) fn generate_main_entity_struct(&self) -> String {
192 let mut fields = Vec::new();
193
194 for section in &self.spec.sections {
195 if !Self::is_root_section(§ion.name)
196 && section.fields.iter().any(|field| field.emit)
197 {
198 let field_name = to_snake_case(§ion.name);
199 let type_name = format!("{}{}", self.entity_name, to_pascal_case(§ion.name));
200 fields.push(format!(
201 " #[serde(default)]\n pub {}: {},",
202 field_name, type_name
203 ));
204 }
205 }
206
207 for section in &self.spec.sections {
208 if Self::is_root_section(§ion.name) {
209 for field in §ion.fields {
210 if !field.emit {
211 continue;
212 }
213 let field_name = to_snake_case(&field.field_name);
214 let rust_type = self.field_type_to_rust(field);
215 fields.push(format!(
216 " #[serde(default)]\n pub {}: {},",
217 field_name, rust_type
218 ));
219 }
220 }
221 }
222
223 format!(
224 "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
225 self.entity_name,
226 fields.join("\n")
227 )
228 }
229
230 pub(crate) fn generate_resolved_types(&self, generated: &mut HashSet<String>) -> String {
231 let mut output = String::new();
232
233 for section in &self.spec.sections {
234 for field in §ion.fields {
235 if !field.emit {
236 continue;
237 }
238 if let Some(resolved) = &field.resolved_type {
239 if generated.insert(resolved.type_name.clone()) {
240 output.push_str("\n\n");
241 output.push_str(&self.generate_resolved_struct(resolved));
242 }
243 }
244 }
245 }
246
247 output
248 }
249
250 fn generate_resolved_struct(&self, resolved: &ResolvedStructType) -> String {
251 if resolved.is_enum {
252 let variants: Vec<String> = resolved
253 .enum_variants
254 .iter()
255 .map(|v| format!(" {},", to_pascal_case(v)))
256 .collect();
257
258 format!(
259 "#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub enum {} {{\n{}\n}}",
260 to_pascal_case(&resolved.type_name),
261 variants.join("\n")
262 )
263 } else {
264 let fields: Vec<String> = resolved
265 .fields
266 .iter()
267 .map(|f| {
268 let rust_type = self.resolved_field_to_rust(f);
269 format!(
270 " #[serde(default)]\n pub {}: {},",
271 to_snake_case(&f.field_name),
272 rust_type
273 )
274 })
275 .collect();
276
277 format!(
278 "#[derive(Debug, Clone, Serialize, Deserialize, Default)]\npub struct {} {{\n{}\n}}",
279 to_pascal_case(&resolved.type_name),
280 fields.join("\n")
281 )
282 }
283 }
284
285 fn generate_event_wrapper(&self) -> String {
286 r#"
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct EventWrapper<T> {
290 #[serde(default)]
291 pub timestamp: i64,
292 pub data: T,
293 #[serde(default)]
294 pub slot: Option<f64>,
295 #[serde(default)]
296 pub signature: Option<String>,
297}
298
299impl<T: Default> Default for EventWrapper<T> {
300 fn default() -> Self {
301 Self {
302 timestamp: 0,
303 data: T::default(),
304 slot: None,
305 signature: None,
306 }
307 }
308}
309"#
310 .to_string()
311 }
312
313 fn generate_entity_rs(&self) -> String {
314 let entity_name = &self.entity_name;
315 let stack_name = self.derive_stack_name();
316 let stack_name_kebab = to_kebab_case(entity_name);
317 let entity_snake = to_snake_case(entity_name);
318
319 let types_import = if self.config.module_mode {
320 "super::types"
321 } else {
322 "crate::types"
323 };
324
325 let url_impl = match &self.config.url {
327 Some(url) => format!(
328 r#"fn url() -> &'static str {{
329 "{}"
330 }}"#,
331 url
332 ),
333 None => r#"fn url() -> &'static str {
334 "" // TODO: Set URL after first deployment in hyperstack.toml
335 }"#
336 .to_string(),
337 };
338
339 let entity_views = self.generate_entity_views_struct();
340
341 format!(
342 r#"use {types_import}::{entity_name};
343use hyperstack_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
344
345pub struct {stack_name}Stack;
346
347impl Stack for {stack_name}Stack {{
348 type Views = {stack_name}StackViews;
349
350 fn name() -> &'static str {{
351 "{stack_name_kebab}"
352 }}
353
354 {url_impl}
355}}
356
357pub struct {stack_name}StackViews {{
358 pub {entity_snake}: {entity_name}EntityViews,
359}}
360
361impl Views for {stack_name}StackViews {{
362 fn from_builder(builder: ViewBuilder) -> Self {{
363 Self {{
364 {entity_snake}: {entity_name}EntityViews {{ builder }},
365 }}
366 }}
367}}
368{entity_views}"#,
369 types_import = types_import,
370 entity_name = entity_name,
371 stack_name = stack_name,
372 stack_name_kebab = stack_name_kebab,
373 entity_snake = entity_snake,
374 url_impl = url_impl,
375 entity_views = entity_views
376 )
377 }
378
379 fn generate_entity_views_struct(&self) -> String {
380 let entity_name = &self.entity_name;
381
382 let derived: Vec<_> = self
383 .spec
384 .views
385 .iter()
386 .filter(|v| {
387 !v.id.ends_with("/state")
388 && !v.id.ends_with("/list")
389 && v.id.starts_with(entity_name)
390 })
391 .collect();
392
393 let mut derived_methods = String::new();
394 for view in &derived {
395 let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
396 let method_name = to_snake_case(view_name);
397
398 derived_methods.push_str(&format!(
399 r#"
400 pub fn {method_name}(&self) -> ViewHandle<{entity_name}> {{
401 self.builder.view("{view_id}")
402 }}
403"#,
404 method_name = method_name,
405 entity_name = entity_name,
406 view_id = view.id
407 ));
408 }
409
410 format!(
411 r#"
412pub struct {entity_name}EntityViews {{
413 builder: ViewBuilder,
414}}
415
416impl {entity_name}EntityViews {{
417 pub fn state(&self) -> StateView<{entity_name}> {{
418 StateView::new(
419 self.builder.connection().clone(),
420 self.builder.store().clone(),
421 "{entity_name}/state".to_string(),
422 self.builder.initial_data_timeout(),
423 )
424 }}
425
426 pub fn list(&self) -> ViewHandle<{entity_name}> {{
427 self.builder.view("{entity_name}/list")
428 }}
429{derived_methods}}}"#,
430 entity_name = entity_name,
431 derived_methods = derived_methods
432 )
433 }
434
435 fn derive_stack_name(&self) -> String {
438 let entity_name = &self.entity_name;
439
440 let suffixes = ["Round", "Token", "Game", "State", "Entity", "Data"];
442
443 for suffix in suffixes {
444 if entity_name.ends_with(suffix) && entity_name.len() > suffix.len() {
445 return entity_name[..entity_name.len() - suffix.len()].to_string();
446 }
447 }
448
449 entity_name.clone()
451 }
452
453 fn field_type_to_rust(&self, field: &FieldTypeInfo) -> String {
467 let base = self.base_type_to_rust(&field.base_type, &field.rust_type_name);
468
469 let typed = if field.is_array && !matches!(field.base_type, BaseType::Array) {
470 format!("Vec<{}>", base)
471 } else {
472 base
473 };
474
475 if field.is_optional {
478 format!("Option<Option<{}>>", typed)
479 } else {
480 format!("Option<{}>", typed)
481 }
482 }
483
484 fn base_type_to_rust(&self, base_type: &BaseType, rust_type_name: &str) -> String {
485 match base_type {
486 BaseType::Integer => {
487 if rust_type_name.contains("u64") {
488 "u64".to_string()
489 } else if rust_type_name.contains("i64") {
490 "i64".to_string()
491 } else if rust_type_name.contains("u32") {
492 "u32".to_string()
493 } else if rust_type_name.contains("i32") {
494 "i32".to_string()
495 } else {
496 "i64".to_string()
497 }
498 }
499 BaseType::Float => "f64".to_string(),
500 BaseType::String => "String".to_string(),
501 BaseType::Boolean => "bool".to_string(),
502 BaseType::Timestamp => "i64".to_string(),
503 BaseType::Binary => "Vec<u8>".to_string(),
504 BaseType::Pubkey => "String".to_string(),
505 BaseType::Array => "Vec<serde_json::Value>".to_string(),
506 BaseType::Object => "serde_json::Value".to_string(),
507 BaseType::Any => "serde_json::Value".to_string(),
508 }
509 }
510
511 fn resolved_field_to_rust(&self, field: &ResolvedField) -> String {
512 let base = self.base_type_to_rust(&field.base_type, &field.field_type);
513
514 let typed = if field.is_array {
515 format!("Vec<{}>", base)
516 } else {
517 base
518 };
519
520 if field.is_optional {
521 format!("Option<Option<{}>>", typed)
522 } else {
523 format!("Option<{}>", typed)
524 }
525 }
526}
527
528#[derive(Debug, Clone)]
533pub struct RustStackConfig {
534 pub crate_name: String,
535 pub sdk_version: String,
536 pub module_mode: bool,
537 pub url: Option<String>,
538}
539
540impl Default for RustStackConfig {
541 fn default() -> Self {
542 Self {
543 crate_name: "generated-stack".to_string(),
544 sdk_version: "0.2".to_string(),
545 module_mode: false,
546 url: None,
547 }
548 }
549}
550
551pub fn compile_stack_spec(
556 stack_spec: SerializableStackSpec,
557 config: Option<RustStackConfig>,
558) -> Result<RustOutput, String> {
559 let config = config.unwrap_or_default();
560 let stack_name = &stack_spec.stack_name;
561 let stack_kebab = to_kebab_case(stack_name);
562
563 let mut entity_names: Vec<String> = Vec::new();
564 let mut entity_specs: Vec<SerializableStreamSpec> = Vec::new();
565
566 for mut spec in stack_spec.entities {
567 if spec.idl.is_none() {
568 spec.idl = stack_spec.idls.first().cloned();
569 }
570 entity_names.push(spec.state_name.clone());
571 entity_specs.push(spec);
572 }
573
574 let types_rs = generate_stack_types_rs(&entity_specs, &entity_names);
575 let entity_rs = generate_stack_entity_rs(
576 stack_name,
577 &stack_kebab,
578 &entity_specs,
579 &entity_names,
580 &config,
581 );
582 let lib_rs = generate_stack_lib_rs(stack_name, &entity_names, config.module_mode);
583 let cargo_toml = generate_stack_cargo_toml(&config);
584
585 Ok(RustOutput {
586 cargo_toml,
587 lib_rs,
588 types_rs,
589 entity_rs,
590 })
591}
592
593fn generate_stack_cargo_toml(config: &RustStackConfig) -> String {
594 format!(
595 r#"[package]
596name = "{}"
597version = "0.1.0"
598edition = "2021"
599
600[dependencies]
601hyperstack-sdk = "{}"
602serde = {{ version = "1", features = ["derive"] }}
603serde_json = "1"
604"#,
605 config.crate_name, config.sdk_version
606 )
607}
608
609fn generate_stack_lib_rs(stack_name: &str, entity_names: &[String], _module_mode: bool) -> String {
610 let entity_views_exports: Vec<String> = entity_names
611 .iter()
612 .map(|name| format!("{}EntityViews", name))
613 .collect();
614
615 let all_exports = format!(
616 "{}Stack, {}StackViews, {}",
617 stack_name,
618 stack_name,
619 entity_views_exports.join(", ")
620 );
621
622 format!(
623 r#"mod entity;
624mod types;
625
626pub use entity::{{{all_exports}}};
627pub use types::*;
628
629pub use hyperstack_sdk::{{ConnectionState, HyperStack, Stack, Update, Views}};
630"#,
631 all_exports = all_exports
632 )
633}
634
635fn generate_stack_types_rs(
637 entity_specs: &[SerializableStreamSpec],
638 entity_names: &[String],
639) -> String {
640 let mut output = String::new();
641 output.push_str("use serde::{Deserialize, Serialize};\n\n");
642
643 let mut generated = HashSet::new();
644
645 for (i, spec) in entity_specs.iter().enumerate() {
646 let entity_name = &entity_names[i];
647 let compiler = RustCompiler::new(spec.clone(), entity_name.clone(), RustConfig::default());
648
649 for section in &spec.sections {
651 if !RustCompiler::is_root_section(§ion.name) {
652 let struct_name = format!("{}{}", entity_name, to_pascal_case(§ion.name));
653 if generated.insert(struct_name) {
654 output.push_str(&compiler.generate_struct_for_section(section));
655 output.push_str("\n\n");
656 }
657 }
658 }
659
660 output.push_str(&compiler.generate_main_entity_struct());
662 output.push_str("\n\n");
663
664 let resolved = compiler.generate_resolved_types(&mut generated);
665 output.push_str(&resolved);
666 while !output.ends_with("\n\n") {
667 output.push('\n');
668 }
669 }
670
671 output.push_str(
673 r#"
674#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct EventWrapper<T> {
676 #[serde(default)]
677 pub timestamp: i64,
678 pub data: T,
679 #[serde(default)]
680 pub slot: Option<f64>,
681 #[serde(default)]
682 pub signature: Option<String>,
683}
684
685impl<T: Default> Default for EventWrapper<T> {
686 fn default() -> Self {
687 Self {
688 timestamp: 0,
689 data: T::default(),
690 slot: None,
691 signature: None,
692 }
693 }
694}
695"#,
696 );
697
698 output
699}
700
701fn generate_stack_entity_rs(
703 stack_name: &str,
704 stack_kebab: &str,
705 entity_specs: &[SerializableStreamSpec],
706 entity_names: &[String],
707 config: &RustStackConfig,
708) -> String {
709 let types_import = if config.module_mode {
710 "super::types"
711 } else {
712 "crate::types"
713 };
714
715 let entity_type_imports: Vec<String> =
716 entity_names.iter().map(|name| name.to_string()).collect();
717
718 let url_impl = match &config.url {
719 Some(url) => format!(
720 r#"fn url() -> &'static str {{
721 "{}"
722 }}"#,
723 url
724 ),
725 None => r#"fn url() -> &'static str {
726 "" // TODO: Set URL after first deployment in hyperstack.toml
727 }"#
728 .to_string(),
729 };
730
731 let views_fields: Vec<String> = entity_names
733 .iter()
734 .map(|name| {
735 let snake = to_snake_case(name);
736 format!(" pub {}: {}EntityViews,", snake, name)
737 })
738 .collect();
739
740 let views_builder_fields: Vec<String> = entity_names
742 .iter()
743 .enumerate()
744 .map(|(i, name)| {
745 let snake = to_snake_case(name);
746 if i < entity_names.len() - 1 {
747 format!(
748 " {}: {}EntityViews {{ builder: builder.clone() }},",
749 snake, name
750 )
751 } else {
752 format!(" {}: {}EntityViews {{ builder }},", snake, name)
753 }
754 })
755 .collect();
756
757 let mut entity_views_structs = Vec::new();
759 for (i, entity_name) in entity_names.iter().enumerate() {
760 let spec = &entity_specs[i];
761
762 let derived: Vec<_> = spec
763 .views
764 .iter()
765 .filter(|v| {
766 !v.id.ends_with("/state")
767 && !v.id.ends_with("/list")
768 && v.id.starts_with(entity_name.as_str())
769 })
770 .collect();
771
772 let mut methods = Vec::new();
773
774 methods.push(format!(
776 r#" pub fn state(&self) -> StateView<{entity}> {{
777 StateView::new(
778 self.builder.connection().clone(),
779 self.builder.store().clone(),
780 "{entity}/state".to_string(),
781 self.builder.initial_data_timeout(),
782 )
783 }}"#,
784 entity = entity_name
785 ));
786
787 methods.push(format!(
789 r#"
790 pub fn list(&self) -> ViewHandle<{entity}> {{
791 self.builder.view("{entity}/list")
792 }}"#,
793 entity = entity_name
794 ));
795
796 for view in &derived {
798 let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
799 let method_name = to_snake_case(view_name);
800 methods.push(format!(
801 r#"
802 pub fn {method}(&self) -> ViewHandle<{entity}> {{
803 self.builder.view("{view_id}")
804 }}"#,
805 method = method_name,
806 entity = entity_name,
807 view_id = view.id
808 ));
809 }
810
811 entity_views_structs.push(format!(
812 r#"
813pub struct {entity}EntityViews {{
814 builder: ViewBuilder,
815}}
816
817impl {entity}EntityViews {{
818{methods}
819}}"#,
820 entity = entity_name,
821 methods = methods.join("\n")
822 ));
823 }
824
825 format!(
826 r#"use {types_import}::{{{entity_imports}}};
827use hyperstack_sdk::{{Stack, StateView, ViewBuilder, ViewHandle, Views}};
828
829pub struct {stack}Stack;
830
831impl Stack for {stack}Stack {{
832 type Views = {stack}StackViews;
833
834 fn name() -> &'static str {{
835 "{stack_kebab}"
836 }}
837
838 {url_impl}
839}}
840
841pub struct {stack}StackViews {{
842{views_fields}
843}}
844
845impl Views for {stack}StackViews {{
846 fn from_builder(builder: ViewBuilder) -> Self {{
847 Self {{
848{views_builder}
849 }}
850 }}
851}}
852{entity_views}"#,
853 types_import = types_import,
854 entity_imports = entity_type_imports.join(", "),
855 stack = stack_name,
856 stack_kebab = stack_kebab,
857 url_impl = url_impl,
858 views_fields = views_fields.join("\n"),
859 views_builder = views_builder_fields.join("\n"),
860 entity_views = entity_views_structs.join("\n"),
861 )
862}
863
864fn to_kebab_case(s: &str) -> String {
865 let mut result = String::new();
866 for (i, c) in s.chars().enumerate() {
867 if c.is_uppercase() {
868 if i > 0 {
869 result.push('-');
870 }
871 result.push(c.to_lowercase().next().unwrap());
872 } else {
873 result.push(c);
874 }
875 }
876 result
877}
878
879fn to_pascal_case(s: &str) -> String {
880 s.split(['_', '-', '.'])
881 .map(|word| {
882 let mut chars = word.chars();
883 match chars.next() {
884 None => String::new(),
885 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
886 }
887 })
888 .collect()
889}
890
891fn to_snake_case(s: &str) -> String {
892 let mut result = String::new();
893 for (i, ch) in s.chars().enumerate() {
894 if ch.is_uppercase() {
895 if i > 0 {
896 result.push('_');
897 }
898 result.push(ch.to_lowercase().next().unwrap());
899 } else {
900 result.push(ch);
901 }
902 }
903 result
904}