use crate::error::{Error, ErrorSet};
use crate::internal::as2::hir::constant_folder::fold_constants;
use crate::internal::as2::hir::optimize_virtual_properties::optimize_virtual_properties;
use crate::internal::as2::hir::register_promoter::promote_variables_to_registers;
use crate::internal::as2::hir::scope::Scope;
use crate::internal::as2::lexer::Lexer;
use crate::internal::as2::resolver::resolve_hir;
use crate::internal::as2::{hir, parser, type_path_to_file_path};
use crate::internal::as2_codegen::{class_to_actions, interface_to_actions, script_to_actions};
use crate::internal::as2_pcode::Actions;
use crate::internal::span::Span;
use crate::provider::SourceProvider;
use crate::sources::SourceSet;
use indexmap::IndexSet;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct SwfOptions {
pub(crate) frame_rate: f32,
pub(crate) stage_x_min: f64,
pub(crate) stage_y_min: f64,
pub(crate) stage_x_max: f64,
pub(crate) stage_y_max: f64,
pub(crate) use_network_sandbox: bool,
}
impl Default for SwfOptions {
fn default() -> Self {
Self {
frame_rate: 24.0,
stage_x_min: 0.0,
stage_y_min: 0.0,
stage_x_max: 100.0,
stage_y_max: 100.0,
use_network_sandbox: false,
}
}
}
impl SwfOptions {
pub fn with_frame_rate(mut self, frame_rate: f32) -> Self {
self.frame_rate = frame_rate;
self
}
pub fn with_stage_size(
mut self,
stage_x_min: f64,
stage_y_min: f64,
stage_x_max: f64,
stage_y_max: f64,
) -> Self {
self.stage_x_min = stage_x_min;
self.stage_y_min = stage_y_min;
self.stage_x_max = stage_x_max;
self.stage_y_max = stage_y_max;
self
}
pub fn with_network_sandbox(mut self, use_network_sandbox: bool) -> Self {
self.use_network_sandbox = use_network_sandbox;
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CompileOptions {
pub(crate) swf_version: u8,
pub(crate) optimizations: OptimizationOptions,
}
impl Default for CompileOptions {
fn default() -> Self {
Self {
swf_version: 15,
optimizations: OptimizationOptions::full(),
}
}
}
impl CompileOptions {
pub fn with_swf_version(mut self, swf_version: u8) -> Self {
self.swf_version = swf_version;
self
}
pub fn with_optimizations(mut self, optimizations: OptimizationOptions) -> Self {
self.optimizations = optimizations;
self
}
}
#[derive(Debug, Serialize)]
pub struct Program {
pub(crate) initial_script: Vec<hir::StatementKind>,
pub(crate) interfaces: Vec<hir::Interface>,
pub(crate) classes: Vec<hir::Class>,
pub(crate) custom_pcodes: Vec<Actions>,
pub(crate) compile_options: CompileOptions,
}
impl Program {
pub(crate) fn optimize(&mut self) {
optimize_virtual_properties(self);
if self
.compile_options
.optimizations
.promote_variables_to_registers
&& self.compile_options.swf_version >= 7
{
promote_variables_to_registers(self);
}
}
pub fn compile(self) -> CompiledProgram {
let initializer = if self.initial_script.is_empty() {
None
} else {
Some(script_to_actions(&self.initial_script))
};
let mut extra_modules = vec![];
for interface in self.interfaces.into_iter().rev() {
let actions = interface_to_actions(&interface);
extra_modules.push((interface.name, actions));
}
for class in self.classes.into_iter().rev() {
let actions = class_to_actions(&self.compile_options, &class);
extra_modules.push((class.name, actions));
}
CompiledProgram {
initializer,
extra_modules,
compile_options: self.compile_options,
custom_pcodes: self.custom_pcodes,
}
}
}
#[derive(Serialize)]
pub struct CompiledProgram {
pub(crate) initializer: Option<Actions>,
pub(crate) extra_modules: Vec<(String, Actions)>,
pub(crate) compile_options: CompileOptions,
pub(crate) custom_pcodes: Vec<Actions>,
}
impl CompiledProgram {
pub fn to_swf(&self, swf_options: &SwfOptions) -> swf::error::Result<Vec<u8>> {
crate::swf::pcode_to_swf(self, swf_options)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct OptimizationOptions {
fold_constants: bool,
promote_variables_to_registers: bool,
}
impl OptimizationOptions {
pub fn full() -> Self {
Self {
fold_constants: true,
promote_variables_to_registers: true,
}
}
pub fn none() -> Self {
Self {
fold_constants: false,
promote_variables_to_registers: false,
}
}
pub fn with_fold_constants(mut self, fold_constants: bool) -> Self {
self.fold_constants = fold_constants;
self
}
pub fn fold_constants(&self) -> bool {
self.fold_constants
}
pub fn with_promote_variables_to_registers(
mut self,
promote_variables_to_registers: bool,
) -> Self {
self.promote_variables_to_registers = promote_variables_to_registers;
self
}
pub fn promote_variables_to_registers(&self) -> bool {
self.promote_variables_to_registers
}
}
pub struct ProgramBuilder<P> {
provider: P,
scripts: Vec<String>,
pcodes: Vec<String>,
classes: Vec<String>,
compile_options: CompileOptions,
}
impl<P> ProgramBuilder<P> {
pub fn add_script(&mut self, path: &str) {
self.scripts.push(path.to_owned());
}
pub fn add_pcode(&mut self, path: &str) {
self.pcodes.push(path.to_owned());
}
pub fn add_class(&mut self, path: &str) {
self.classes.push(path.to_owned());
}
pub fn with_compile_options(mut self, compile_options: CompileOptions) -> Self {
self.compile_options = compile_options;
self
}
}
impl<P: SourceProvider> ProgramBuilder<P> {
pub fn new(provider: P) -> Self {
Self {
provider,
scripts: vec![],
pcodes: vec![],
classes: vec![],
compile_options: CompileOptions::default(),
}
}
pub fn build(self) -> Result<Program, Error> {
let mut root_scope = Scope::default();
let mut initial_script = vec![];
let mut interfaces = vec![];
let mut classes = vec![];
let mut errors = ErrorSet::new();
let mut loaded_classes = self.classes.iter().cloned().collect();
let mut pending_classes = self.classes;
let mut custom_pcodes = vec![];
let known_script_paths = self.scripts.iter().cloned().collect();
let mut source_set = SourceSet::new();
#[expect(clippy::too_many_arguments)]
fn load_actionscript<P: SourceProvider>(
provider: &P,
errors: &mut ErrorSet,
loaded_classes: &mut IndexSet<String>,
pending_classes: &mut Vec<String>,
path: &str,
type_name: &str,
is_script: bool,
known_script_paths: &IndexSet<String>,
compile_options: &CompileOptions,
source_set: &mut SourceSet,
) -> Option<hir::Document> {
let source = match source_set.get_or_load(path.to_owned(), provider) {
Ok(source) => source,
Err(e) => {
errors.add_io_error(path, e);
return None;
}
};
let tokens = Lexer::new(&source.source, source.file_id).into_vec();
let ast = match parser::parse_document(&tokens) {
Ok(ast) => ast,
Err(e) => {
errors.add_parsing_error(e.into());
return None;
}
};
let (mut hir, hir_errors, dependencies) = resolve_hir(
provider,
source_set,
ast,
is_script,
type_name,
known_script_paths,
);
for error in hir_errors {
errors.add_parsing_error(error);
}
for name in dependencies {
if loaded_classes.insert(name.to_owned()) {
pending_classes.push(name);
}
}
if compile_options.optimizations.fold_constants {
while fold_constants(&mut hir) {
}
}
Some(hir)
}
fn load_pcode<P: SourceProvider>(
provider: &P,
errors: &mut ErrorSet,
path: &str,
custom_pcodes: &mut Vec<Actions>,
source_set: &mut SourceSet,
) {
let source = match source_set.get_or_load(path.to_owned(), provider) {
Ok(source) => source,
Err(e) => {
errors.add_io_error(path, e);
return;
}
};
let tokens =
crate::internal::as2_pcode::Lexer::new(&source.source, source.file_id).into_vec();
match crate::internal::as2_pcode::parse_actions(&tokens) {
Ok(actions) => custom_pcodes.push(actions),
Err(e) => errors.add_parsing_error(e.into()),
}
}
for path in self.pcodes {
load_pcode(
&self.provider,
&mut errors,
&path,
&mut custom_pcodes,
&mut source_set,
);
}
for path in self.scripts {
if let Some(document) = load_actionscript(
&self.provider,
&mut errors,
&mut loaded_classes,
&mut pending_classes,
&path,
"",
true,
&known_script_paths,
&self.compile_options,
&mut source_set,
) && let hir::Document::Script { statements, scope } = document
{
root_scope.defined_variables.extend(scope.defined_variables);
root_scope
.referenced_variables
.extend(scope.referenced_variables);
root_scope.could_reference_anything |= scope.could_reference_anything;
initial_script.extend(statements);
}
}
let mut entry_point_class = vec![];
while let Some(type_name) = pending_classes.pop() {
let filename = type_path_to_file_path(&type_name);
if let Some(document) = load_actionscript(
&self.provider,
&mut errors,
&mut loaded_classes,
&mut pending_classes,
&filename,
&type_name,
false,
&known_script_paths,
&self.compile_options,
&mut source_set,
) {
match document {
hir::Document::Interface(interface) => {
interfaces.push(interface);
}
hir::Document::Class(class) => {
if let Some(main) = class.functions.get("main")
&& main.is_static
{
entry_point_class.push(class.name.clone());
}
classes.push(*class);
}
_ => {}
}
}
}
match entry_point_class.len() {
0 => {} 1 => initial_script.push(call_main_method(entry_point_class.first().unwrap())),
_ => errors.add_misc_error(format!(
"Conflicting entry points found on classes: {}",
entry_point_class.join(", ")
)),
};
errors.error_unless_empty(source_set)?;
let mut program = Program {
initial_script,
interfaces,
classes,
custom_pcodes,
compile_options: self.compile_options,
};
program.optimize();
Ok(program)
}
}
fn call_main_method(class: &str) -> hir::StatementKind {
let mut path = class.split(".").collect::<Vec<&str>>();
path.push("main");
let path: Vec<String> = path.into_iter().map(|s| s.to_owned()).collect();
let mut name = None;
for part in path.into_iter() {
if let Some(prev) = name.take() {
name = Some(hir::Expr::new(
Span::default(),
hir::ExprKind::Field(
Box::new(prev),
Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::String(part)),
)),
),
));
} else {
name = Some(hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::Identifier(part)),
));
}
}
let name = name.unwrap();
hir::StatementKind::Expr(hir::Expr::new(
Span::default(),
hir::ExprKind::Call {
name: Box::new(name),
args: vec![hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::Identifier("this".to_owned())),
)],
},
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::FileSystemSourceProvider;
#[test]
fn test_all_samples() {
insta::glob!("../../../samples/as2", "**/*.as", |path| {
let mut builder = ProgramBuilder::new(FileSystemSourceProvider::with_root(
path.parent().unwrap().to_owned(),
));
builder.add_script(path.file_name().unwrap().to_str().unwrap());
let parsed = builder.build();
insta::assert_yaml_snapshot!(parsed);
});
insta::glob!("../../../samples/as2_classes", "*.as", |path| {
let mut builder = ProgramBuilder::new(FileSystemSourceProvider::with_root(
path.parent().unwrap().to_owned(),
));
builder.add_class(
path.file_name()
.unwrap()
.to_str()
.unwrap()
.strip_suffix(".as")
.unwrap(),
);
let parsed = builder.build();
insta::assert_yaml_snapshot!(parsed);
});
}
#[test]
fn test_fail_samples() {
insta::glob!("../../../samples/as2_errors", "**/*.as", |path| {
let mut builder = ProgramBuilder::new(FileSystemSourceProvider::with_root(
path.parent().unwrap().to_owned(),
));
builder.add_script(path.file_name().unwrap().to_str().unwrap());
let parsed = builder.build().unwrap_err();
insta::assert_snapshot!(parsed);
});
}
#[test]
fn test_main_method_simple() {
assert_eq!(
call_main_method("foo"),
hir::StatementKind::Expr(hir::Expr::new(
Span::default(),
hir::ExprKind::Call {
name: Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Field(
Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::Identifier(
"foo".to_owned()
))
)),
Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::String(
"main".to_owned()
))
))
)
)),
args: vec![hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::Identifier("this".to_owned()))
)]
}
))
);
}
#[test]
fn test_main_method_path() {
assert_eq!(
call_main_method("foo.bar.baz"),
hir::StatementKind::Expr(hir::Expr::new(
Span::default(),
hir::ExprKind::Call {
name: Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Field(
Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Field(
Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Field(
Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(
hir::ConstantKind::Identifier("foo".to_owned())
)
)),
Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::String(
"bar".to_owned()
))
))
)
)),
Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::String(
"baz".to_owned()
))
)),
)
)),
Box::new(hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::String(
"main".to_owned()
))
))
)
)),
args: vec![hir::Expr::new(
Span::default(),
hir::ExprKind::Constant(hir::ConstantKind::Identifier("this".to_owned()))
)]
}
))
);
}
}