Skip to main content

rascal/
program.rs

1use crate::error::{Error, ErrorSet};
2use crate::internal::as2::hir::constant_folder::fold_constants;
3use crate::internal::as2::hir::optimize_virtual_properties::optimize_virtual_properties;
4use crate::internal::as2::hir::register_promoter::promote_variables_to_registers;
5use crate::internal::as2::hir::scope::Scope;
6use crate::internal::as2::lexer::Lexer;
7use crate::internal::as2::resolver::resolve_hir;
8use crate::internal::as2::{hir, parser, type_path_to_file_path};
9use crate::internal::as2_codegen::{class_to_actions, interface_to_actions, script_to_actions};
10use crate::internal::as2_pcode::Actions;
11use crate::internal::span::Span;
12use crate::provider::SourceProvider;
13use crate::sources::SourceSet;
14use indexmap::IndexSet;
15use serde::Serialize;
16
17#[derive(Debug, Clone, Serialize)]
18pub struct SwfOptions {
19    pub(crate) frame_rate: f32,
20    pub(crate) stage_x_min: f64,
21    pub(crate) stage_y_min: f64,
22    pub(crate) stage_x_max: f64,
23    pub(crate) stage_y_max: f64,
24    pub(crate) use_network_sandbox: bool,
25}
26
27impl Default for SwfOptions {
28    fn default() -> Self {
29        Self {
30            frame_rate: 24.0,
31            stage_x_min: 0.0,
32            stage_y_min: 0.0,
33            stage_x_max: 100.0,
34            stage_y_max: 100.0,
35            use_network_sandbox: false,
36        }
37    }
38}
39
40impl SwfOptions {
41    pub fn with_frame_rate(mut self, frame_rate: f32) -> Self {
42        self.frame_rate = frame_rate;
43        self
44    }
45
46    pub fn with_stage_size(
47        mut self,
48        stage_x_min: f64,
49        stage_y_min: f64,
50        stage_x_max: f64,
51        stage_y_max: f64,
52    ) -> Self {
53        self.stage_x_min = stage_x_min;
54        self.stage_y_min = stage_y_min;
55        self.stage_x_max = stage_x_max;
56        self.stage_y_max = stage_y_max;
57        self
58    }
59
60    pub fn with_network_sandbox(mut self, use_network_sandbox: bool) -> Self {
61        self.use_network_sandbox = use_network_sandbox;
62        self
63    }
64}
65
66#[derive(Debug, Clone, Serialize)]
67pub struct CompileOptions {
68    pub(crate) swf_version: u8,
69    pub(crate) optimizations: OptimizationOptions,
70}
71
72impl Default for CompileOptions {
73    fn default() -> Self {
74        Self {
75            swf_version: 15,
76            optimizations: OptimizationOptions::full(),
77        }
78    }
79}
80
81impl CompileOptions {
82    pub fn with_swf_version(mut self, swf_version: u8) -> Self {
83        self.swf_version = swf_version;
84        self
85    }
86
87    pub fn with_optimizations(mut self, optimizations: OptimizationOptions) -> Self {
88        self.optimizations = optimizations;
89        self
90    }
91}
92
93#[derive(Debug, Serialize)]
94pub struct Program {
95    pub(crate) initial_script: Vec<hir::StatementKind>,
96    pub(crate) interfaces: Vec<hir::Interface>,
97    pub(crate) classes: Vec<hir::Class>,
98    pub(crate) custom_pcodes: Vec<Actions>,
99    pub(crate) compile_options: CompileOptions,
100}
101
102impl Program {
103    pub(crate) fn optimize(&mut self) {
104        optimize_virtual_properties(self);
105        if self
106            .compile_options
107            .optimizations
108            .promote_variables_to_registers
109            && self.compile_options.swf_version >= 7
110        {
111            promote_variables_to_registers(self);
112        }
113    }
114
115    pub fn compile(self) -> CompiledProgram {
116        let initializer = if self.initial_script.is_empty() {
117            None
118        } else {
119            Some(script_to_actions(&self.initial_script))
120        };
121        let mut extra_modules = vec![];
122        for interface in self.interfaces.into_iter().rev() {
123            let actions = interface_to_actions(&interface);
124            extra_modules.push((interface.name, actions));
125        }
126        for class in self.classes.into_iter().rev() {
127            let actions = class_to_actions(&self.compile_options, &class);
128            extra_modules.push((class.name, actions));
129        }
130        CompiledProgram {
131            initializer,
132            extra_modules,
133            compile_options: self.compile_options,
134            custom_pcodes: self.custom_pcodes,
135        }
136    }
137}
138
139#[derive(Serialize)]
140pub struct CompiledProgram {
141    pub(crate) initializer: Option<Actions>,
142    pub(crate) extra_modules: Vec<(String, Actions)>,
143    pub(crate) compile_options: CompileOptions,
144    pub(crate) custom_pcodes: Vec<Actions>,
145}
146
147impl CompiledProgram {
148    pub fn to_swf(&self, swf_options: &SwfOptions) -> swf::error::Result<Vec<u8>> {
149        crate::swf::pcode_to_swf(self, swf_options)
150    }
151}
152
153#[derive(Debug, Clone, Serialize)]
154pub struct OptimizationOptions {
155    fold_constants: bool,
156    promote_variables_to_registers: bool,
157}
158
159impl OptimizationOptions {
160    pub fn full() -> Self {
161        Self {
162            fold_constants: true,
163            promote_variables_to_registers: true,
164        }
165    }
166
167    pub fn none() -> Self {
168        Self {
169            fold_constants: false,
170            promote_variables_to_registers: false,
171        }
172    }
173
174    pub fn with_fold_constants(mut self, fold_constants: bool) -> Self {
175        self.fold_constants = fold_constants;
176        self
177    }
178
179    pub fn fold_constants(&self) -> bool {
180        self.fold_constants
181    }
182
183    pub fn with_promote_variables_to_registers(
184        mut self,
185        promote_variables_to_registers: bool,
186    ) -> Self {
187        self.promote_variables_to_registers = promote_variables_to_registers;
188        self
189    }
190
191    pub fn promote_variables_to_registers(&self) -> bool {
192        self.promote_variables_to_registers
193    }
194}
195
196pub struct ProgramBuilder<P> {
197    provider: P,
198    scripts: Vec<String>,
199    pcodes: Vec<String>,
200    classes: Vec<String>,
201    compile_options: CompileOptions,
202}
203
204impl<P> ProgramBuilder<P> {
205    pub fn add_script(&mut self, path: &str) {
206        self.scripts.push(path.to_owned());
207    }
208
209    pub fn add_pcode(&mut self, path: &str) {
210        self.pcodes.push(path.to_owned());
211    }
212
213    pub fn add_class(&mut self, path: &str) {
214        self.classes.push(path.to_owned());
215    }
216
217    pub fn with_compile_options(mut self, compile_options: CompileOptions) -> Self {
218        self.compile_options = compile_options;
219        self
220    }
221}
222
223impl<P: SourceProvider> ProgramBuilder<P> {
224    pub fn new(provider: P) -> Self {
225        Self {
226            provider,
227            scripts: vec![],
228            pcodes: vec![],
229            classes: vec![],
230            compile_options: CompileOptions::default(),
231        }
232    }
233
234    pub fn build(self) -> Result<Program, Error> {
235        let mut root_scope = Scope::default();
236        let mut initial_script = vec![];
237        let mut interfaces = vec![];
238        let mut classes = vec![];
239        let mut errors = ErrorSet::new();
240        let mut loaded_classes = self.classes.iter().cloned().collect();
241        let mut pending_classes = self.classes;
242        let mut custom_pcodes = vec![];
243        let known_script_paths = self.scripts.iter().cloned().collect();
244        let mut source_set = SourceSet::new();
245
246        #[expect(clippy::too_many_arguments)]
247        fn load_actionscript<P: SourceProvider>(
248            provider: &P,
249            errors: &mut ErrorSet,
250            loaded_classes: &mut IndexSet<String>,
251            pending_classes: &mut Vec<String>,
252            path: &str,
253            type_name: &str,
254            is_script: bool,
255            known_script_paths: &IndexSet<String>,
256            compile_options: &CompileOptions,
257            source_set: &mut SourceSet,
258        ) -> Option<hir::Document> {
259            let source = match source_set.get_or_load(path.to_owned(), provider) {
260                Ok(source) => source,
261                Err(e) => {
262                    errors.add_io_error(path, e);
263                    return None;
264                }
265            };
266            let tokens = Lexer::new(&source.source, source.file_id).into_vec();
267            let ast = match parser::parse_document(&tokens) {
268                Ok(ast) => ast,
269                Err(e) => {
270                    errors.add_parsing_error(e.into());
271                    return None;
272                }
273            };
274            let (mut hir, hir_errors, dependencies) = resolve_hir(
275                provider,
276                source_set,
277                ast,
278                is_script,
279                type_name,
280                known_script_paths,
281            );
282            for error in hir_errors {
283                errors.add_parsing_error(error);
284            }
285            for name in dependencies {
286                if loaded_classes.insert(name.to_owned()) {
287                    pending_classes.push(name);
288                }
289            }
290            if compile_options.optimizations.fold_constants {
291                while fold_constants(&mut hir) {
292                    // Keep going until nothing changed
293                }
294            }
295            Some(hir)
296        }
297
298        fn load_pcode<P: SourceProvider>(
299            provider: &P,
300            errors: &mut ErrorSet,
301            path: &str,
302            custom_pcodes: &mut Vec<Actions>,
303            source_set: &mut SourceSet,
304        ) {
305            let source = match source_set.get_or_load(path.to_owned(), provider) {
306                Ok(source) => source,
307                Err(e) => {
308                    errors.add_io_error(path, e);
309                    return;
310                }
311            };
312            let tokens =
313                crate::internal::as2_pcode::Lexer::new(&source.source, source.file_id).into_vec();
314            match crate::internal::as2_pcode::parse_actions(&tokens) {
315                Ok(actions) => custom_pcodes.push(actions),
316                Err(e) => errors.add_parsing_error(e.into()),
317            }
318        }
319
320        for path in self.pcodes {
321            load_pcode(
322                &self.provider,
323                &mut errors,
324                &path,
325                &mut custom_pcodes,
326                &mut source_set,
327            );
328        }
329
330        for path in self.scripts {
331            if let Some(document) = load_actionscript(
332                &self.provider,
333                &mut errors,
334                &mut loaded_classes,
335                &mut pending_classes,
336                &path,
337                "",
338                true,
339                &known_script_paths,
340                &self.compile_options,
341                &mut source_set,
342            ) && let hir::Document::Script { statements, scope } = document
343            {
344                root_scope.defined_variables.extend(scope.defined_variables);
345                root_scope
346                    .referenced_variables
347                    .extend(scope.referenced_variables);
348                root_scope.could_reference_anything |= scope.could_reference_anything;
349                initial_script.extend(statements);
350            }
351        }
352
353        let mut entry_point_class = vec![];
354
355        while let Some(type_name) = pending_classes.pop() {
356            let filename = type_path_to_file_path(&type_name);
357            if let Some(document) = load_actionscript(
358                &self.provider,
359                &mut errors,
360                &mut loaded_classes,
361                &mut pending_classes,
362                &filename,
363                &type_name,
364                false,
365                &known_script_paths,
366                &self.compile_options,
367                &mut source_set,
368            ) {
369                match document {
370                    hir::Document::Interface(interface) => {
371                        interfaces.push(interface);
372                    }
373                    hir::Document::Class(class) => {
374                        if let Some(main) = class.functions.get("main")
375                            && main.is_static
376                        {
377                            entry_point_class.push(class.name.clone());
378                        }
379                        classes.push(*class);
380                    }
381                    _ => {}
382                }
383            }
384        }
385
386        match entry_point_class.len() {
387            0 => {} // It's fine to have no entry point... even if it _may_ not entirely make sense :D
388            1 => initial_script.push(call_main_method(entry_point_class.first().unwrap())),
389            _ => errors.add_misc_error(format!(
390                "Conflicting entry points found on classes: {}",
391                entry_point_class.join(", ")
392            )),
393        };
394
395        errors.error_unless_empty(source_set)?;
396
397        let mut program = Program {
398            initial_script,
399            interfaces,
400            classes,
401            custom_pcodes,
402            compile_options: self.compile_options,
403        };
404        program.optimize();
405        Ok(program)
406    }
407}
408
409fn call_main_method(class: &str) -> hir::StatementKind {
410    let mut path = class.split(".").collect::<Vec<&str>>();
411    path.push("main");
412    let path: Vec<String> = path.into_iter().map(|s| s.to_owned()).collect();
413
414    let mut name = None;
415    for part in path.into_iter() {
416        if let Some(prev) = name.take() {
417            name = Some(hir::Expr::new(
418                Span::default(),
419                hir::ExprKind::Field(
420                    Box::new(prev),
421                    Box::new(hir::Expr::new(
422                        Span::default(),
423                        hir::ExprKind::Constant(hir::ConstantKind::String(part)),
424                    )),
425                ),
426            ));
427        } else {
428            name = Some(hir::Expr::new(
429                Span::default(),
430                hir::ExprKind::Constant(hir::ConstantKind::Identifier(part)),
431            ));
432        }
433    }
434    // Name has to be something, as we always added 'main' to the path
435    let name = name.unwrap();
436
437    hir::StatementKind::Expr(hir::Expr::new(
438        Span::default(),
439        hir::ExprKind::Call {
440            name: Box::new(name),
441            args: vec![hir::Expr::new(
442                Span::default(),
443                hir::ExprKind::Constant(hir::ConstantKind::Identifier("this".to_owned())),
444            )],
445        },
446    ))
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use crate::provider::FileSystemSourceProvider;
453
454    #[test]
455    fn test_all_samples() {
456        insta::glob!("../../../samples/as2", "**/*.as", |path| {
457            let mut builder = ProgramBuilder::new(FileSystemSourceProvider::with_root(
458                path.parent().unwrap().to_owned(),
459            ));
460            builder.add_script(path.file_name().unwrap().to_str().unwrap());
461            let parsed = builder.build();
462            insta::assert_yaml_snapshot!(parsed);
463        });
464        insta::glob!("../../../samples/as2_classes", "*.as", |path| {
465            let mut builder = ProgramBuilder::new(FileSystemSourceProvider::with_root(
466                path.parent().unwrap().to_owned(),
467            ));
468            builder.add_class(
469                path.file_name()
470                    .unwrap()
471                    .to_str()
472                    .unwrap()
473                    .strip_suffix(".as")
474                    .unwrap(),
475            );
476            let parsed = builder.build();
477            insta::assert_yaml_snapshot!(parsed);
478        });
479    }
480
481    #[test]
482    fn test_fail_samples() {
483        insta::glob!("../../../samples/as2_errors", "**/*.as", |path| {
484            let mut builder = ProgramBuilder::new(FileSystemSourceProvider::with_root(
485                path.parent().unwrap().to_owned(),
486            ));
487            builder.add_script(path.file_name().unwrap().to_str().unwrap());
488            let parsed = builder.build().unwrap_err();
489            insta::assert_snapshot!(parsed);
490        });
491    }
492
493    #[test]
494    fn test_main_method_simple() {
495        assert_eq!(
496            call_main_method("foo"),
497            hir::StatementKind::Expr(hir::Expr::new(
498                Span::default(),
499                hir::ExprKind::Call {
500                    name: Box::new(hir::Expr::new(
501                        Span::default(),
502                        hir::ExprKind::Field(
503                            Box::new(hir::Expr::new(
504                                Span::default(),
505                                hir::ExprKind::Constant(hir::ConstantKind::Identifier(
506                                    "foo".to_owned()
507                                ))
508                            )),
509                            Box::new(hir::Expr::new(
510                                Span::default(),
511                                hir::ExprKind::Constant(hir::ConstantKind::String(
512                                    "main".to_owned()
513                                ))
514                            ))
515                        )
516                    )),
517                    args: vec![hir::Expr::new(
518                        Span::default(),
519                        hir::ExprKind::Constant(hir::ConstantKind::Identifier("this".to_owned()))
520                    )]
521                }
522            ))
523        );
524    }
525
526    #[test]
527    fn test_main_method_path() {
528        assert_eq!(
529            call_main_method("foo.bar.baz"),
530            hir::StatementKind::Expr(hir::Expr::new(
531                Span::default(),
532                hir::ExprKind::Call {
533                    name: Box::new(hir::Expr::new(
534                        Span::default(),
535                        hir::ExprKind::Field(
536                            Box::new(hir::Expr::new(
537                                Span::default(),
538                                hir::ExprKind::Field(
539                                    Box::new(hir::Expr::new(
540                                        Span::default(),
541                                        hir::ExprKind::Field(
542                                            Box::new(hir::Expr::new(
543                                                Span::default(),
544                                                hir::ExprKind::Constant(
545                                                    hir::ConstantKind::Identifier("foo".to_owned())
546                                                )
547                                            )),
548                                            Box::new(hir::Expr::new(
549                                                Span::default(),
550                                                hir::ExprKind::Constant(hir::ConstantKind::String(
551                                                    "bar".to_owned()
552                                                ))
553                                            ))
554                                        )
555                                    )),
556                                    Box::new(hir::Expr::new(
557                                        Span::default(),
558                                        hir::ExprKind::Constant(hir::ConstantKind::String(
559                                            "baz".to_owned()
560                                        ))
561                                    )),
562                                )
563                            )),
564                            Box::new(hir::Expr::new(
565                                Span::default(),
566                                hir::ExprKind::Constant(hir::ConstantKind::String(
567                                    "main".to_owned()
568                                ))
569                            ))
570                        )
571                    )),
572                    args: vec![hir::Expr::new(
573                        Span::default(),
574                        hir::ExprKind::Constant(hir::ConstantKind::Identifier("this".to_owned()))
575                    )]
576                }
577            ))
578        );
579    }
580}