Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 93 additions & 93 deletions src/ast/sprite.rs
Original file line number Diff line number Diff line change
@@ -1,93 +1,93 @@
use fxhash::{
FxHashMap,
FxHashSet,
};
use logos::Span;
use serde::{
Deserialize,
Serialize,
};
use super::*;
use crate::{
diagnostic::{
Diagnostic,
DiagnosticKind,
},
misc::SmolStr,
};
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Sprite {
pub costumes: Vec<Costume>,
pub sounds: Vec<Sound>,
pub procs: FxHashMap<SmolStr, Proc>,
pub proc_definitions: FxHashMap<SmolStr, Vec<Stmt>>,
pub proc_references: FxHashMap<SmolStr, References>,
pub proc_args: FxHashMap<SmolStr, Vec<Arg>>,
pub funcs: FxHashMap<SmolStr, Func>,
pub func_definitions: FxHashMap<SmolStr, Vec<Stmt>>,
pub func_references: FxHashMap<SmolStr, References>,
pub func_args: FxHashMap<SmolStr, Vec<Arg>>,
pub enums: FxHashMap<SmolStr, Enum>,
pub structs: FxHashMap<SmolStr, Struct>,
pub vars: FxHashMap<SmolStr, Var>,
pub proc_locals: FxHashMap<SmolStr, FxHashMap<SmolStr, Var>>,
pub func_locals: FxHashMap<SmolStr, FxHashMap<SmolStr, Var>>,
pub lists: FxHashMap<SmolStr, List>,
pub events: Vec<Event>,
pub used_procs: FxHashSet<SmolStr>,
pub used_funcs: FxHashSet<SmolStr>,
pub volume: Option<(Value, Span)>,
pub layer_order: Option<(Value, Span)>,
pub x_position: Option<(Value, Span)>,
pub y_position: Option<(Value, Span)>,
pub size: Option<(Value, Span)>,
pub direction: Option<(Value, Span)>,
pub rotation_style: RotationStyle,
pub hidden: bool,
}
impl Sprite {
pub fn add_proc(
&mut self,
proc: Proc,
args: Vec<Arg>,
stmts: Vec<Stmt>,
diagnostics: &mut Vec<Diagnostic>,
) {
let name = proc.name.clone();
if self.procs.contains_key(&name) {
diagnostics.push(Diagnostic {
kind: DiagnosticKind::ProcedureRedefinition(name),
span: proc.span.clone(),
});
return;
}
self.procs.insert(name.clone(), proc);
self.proc_args.insert(name.clone(), args);
self.proc_definitions.insert(name.clone(), stmts);
self.proc_references.insert(name, Default::default());
}
pub fn add_func(
&mut self,
func: Func,
args: Vec<Arg>,
stmts: Vec<Stmt>,
diagnostics: &mut Vec<Diagnostic>,
) {
let name = func.name.clone();
if self.funcs.contains_key(&name) {
diagnostics.push(Diagnostic {
kind: DiagnosticKind::FunctionRedefinition(name),
span: func.span.clone(),
});
return;
}
self.funcs.insert(name.clone(), func);
self.func_args.insert(name.clone(), args);
self.func_definitions.insert(name.clone(), stmts);
self.func_references.insert(name, Default::default());
}
}
use fxhash::{
FxHashMap,
FxHashSet,
};
use logos::Span;
use serde::{
Deserialize,
Serialize,
};

use super::*;
use crate::{
diagnostic::{
Diagnostic,
DiagnosticKind,
},
misc::SmolStr,
};

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Sprite {
pub costumes: Vec<Costume>,
pub sounds: Vec<Sound>,
pub procs: FxHashMap<SmolStr, Proc>,
pub proc_definitions: FxHashMap<SmolStr, Vec<Stmt>>,
pub proc_references: FxHashMap<SmolStr, References>,
pub proc_args: FxHashMap<SmolStr, Vec<Arg>>,
pub funcs: FxHashMap<SmolStr, Func>,
pub func_definitions: FxHashMap<SmolStr, Vec<Stmt>>,
pub func_references: FxHashMap<SmolStr, References>,
pub func_args: FxHashMap<SmolStr, Vec<Arg>>,
pub enums: FxHashMap<SmolStr, Enum>,
pub structs: FxHashMap<SmolStr, Struct>,
pub vars: FxHashMap<SmolStr, Var>,
pub proc_locals: FxHashMap<SmolStr, FxHashMap<SmolStr, Var>>,
pub func_locals: FxHashMap<SmolStr, FxHashMap<SmolStr, Var>>,
pub lists: FxHashMap<SmolStr, List>,
pub events: Vec<Event>,
pub used_procs: FxHashSet<SmolStr>,
pub used_funcs: FxHashSet<SmolStr>,
pub volume: Option<(Value, Span)>,
pub layer_order: Option<(Value, Span)>,
pub x_position: Option<(Value, Span)>,
pub y_position: Option<(Value, Span)>,
pub size: Option<(Value, Span)>,
pub direction: Option<(Value, Span)>,
pub rotation_style: RotationStyle,
pub hidden: bool,
}

impl Sprite {
pub fn add_proc(
&mut self,
proc: Proc,
args: Vec<Arg>,
stmts: Vec<Stmt>,
diagnostics: &mut Vec<Diagnostic>,
) {
let name = proc.name.clone();
if self.procs.contains_key(&name) {
diagnostics.push(Diagnostic {
kind: DiagnosticKind::ProcedureRedefinition(name),
span: proc.span.clone(),
});
return;
}
self.procs.insert(name.clone(), proc);
self.proc_args.insert(name.clone(), args);
self.proc_definitions.insert(name.clone(), stmts);
self.proc_references.insert(name, Default::default());
}

pub fn add_func(
&mut self,
func: Func,
args: Vec<Arg>,
stmts: Vec<Stmt>,
diagnostics: &mut Vec<Diagnostic>,
) {
let name = func.name.clone();
if self.funcs.contains_key(&name) {
diagnostics.push(Diagnostic {
kind: DiagnosticKind::FunctionRedefinition(name),
span: func.span.clone(),
});
return;
}
self.funcs.insert(name.clone(), func);
self.func_args.insert(name.clone(), args);
self.func_definitions.insert(name.clone(), stmts);
self.func_references.insert(name, Default::default());
}
}
144 changes: 142 additions & 2 deletions src/codegen/datalists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use std::{
BufRead,
BufReader,
},
path::Path,
path::{
Path,
PathBuf,
},
rc::Rc,
};

Expand All @@ -15,15 +18,77 @@ use crate::{
vfs::VFS,
};

fn resolve_path(base_path: Option<&Path>, path: &str, input: &Path) -> PathBuf {
// Handle absolute paths
if Path::new(path).is_absolute() {
return PathBuf::from(path);
}

// "./" prefix: relative to sprite directory (or input for stage)
if let Some(stripped) = path.strip_prefix("./") {
let base_dir = if let Some(base_path) = base_path {
base_path.parent().unwrap_or_else(|| Path::new("."))
} else {
input.parent().unwrap_or(input)
};
return normalize_path(base_dir.join(stripped));
}

// For sprites: any path starting with "../" should also be relative to sprite directory
if path.starts_with("../") && base_path.is_some() {
let base_dir = base_path
.unwrap()
.parent()
.unwrap_or_else(|| Path::new("."));
return normalize_path(base_dir.join(path));
}

// No prefix: relative to project root (input's parent directory or input itself)
let project_root = input.parent().unwrap_or(input);
normalize_path(project_root.join(path))
}

fn normalize_path(path: PathBuf) -> PathBuf {
// Try to normalize manually first for more predictable behavior in tests
let mut result = PathBuf::new();

for component in path.components() {
match component {
std::path::Component::ParentDir => {
// Only pop if we have something to pop and it's not already at root
if !result.as_os_str().is_empty()
&& result
.components()
.last()
.map(|c| !matches!(c, std::path::Component::ParentDir))
.unwrap_or(false)
{
result.pop();
} else {
// If we can't go up further, keep the parent dir
result.push("..");
}
}
std::path::Component::CurDir => {
// Skip current directory components
}
_ => result.push(component),
}
}
result
}

pub fn read_list(
fs: Rc<RefCell<dyn VFS>>,
input: &Path,
path: &SmolStr,
current_file_path: &Path,
) -> Result<Vec<SmolStr>, DiagnosticKind> {
let (_, ext) = path.rsplit_once('.').unwrap_or_default();
let resolved_path = resolve_path(Some(current_file_path), path, input);
let mut fs = fs.borrow_mut();
let mut file = fs
.read_file(&input.join(&**path))
.read_file(&resolved_path)
.map_err(|err| DiagnosticKind::IOError(err.to_string().into()))?;
match ext {
_ => read_list_text(&mut file),
Expand All @@ -35,3 +100,78 @@ fn read_list_text(file: &mut Box<dyn io::Read + '_>) -> Result<Vec<SmolStr>, io:
let file = BufReader::new(file);
Ok(file.lines().into_iter().flatten().map(Into::into).collect())
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;

use super::*;

#[test]
fn test_resolve_path_relative_to_project_root() {
let base_path = PathBuf::from("/project/sprites/sprite.gbo");
let input = PathBuf::from("/project/stage.gbo");

// Test no prefix: relative to project root
let result = resolve_path(Some(&base_path), "data.txt", &input);
assert_eq!(result, PathBuf::from("/project/data.txt"));
}

#[test]
fn test_resolve_path_current_dir() {
let base_path = PathBuf::from("/project/sprites/sprite.gbo");
let input = PathBuf::from("/project/stage.gbo");

// Test ./ prefix: relative to sprite directory
let result = resolve_path(Some(&base_path), "./data.txt", &input);
assert_eq!(result, PathBuf::from("/project/sprites/data.txt"));
}

#[test]
fn test_resolve_path_parent_dir() {
let base_path = PathBuf::from("/project/sprites/sprite.gbo");
let input = PathBuf::from("/project/stage.gbo");

// Test ../ prefix: relative to sprite directory, go up one level
let result = resolve_path(Some(&base_path), "../data.txt", &input);
assert_eq!(result, PathBuf::from("/project/data.txt"));
}

#[test]
fn test_resolve_path_multiple_parents() {
let base_path = PathBuf::from("/project/sprites/subdir/sprite.gbo");
let input = PathBuf::from("/project/stage.gbo");

// Test multiple ../: relative to sprite directory, go up multiple levels
let result = resolve_path(Some(&base_path), "../../data.txt", &input);
assert_eq!(result, PathBuf::from("/project/data.txt"));
}

#[test]
fn test_resolve_path_no_base() {
let input = PathBuf::from("/project/stage.gbo");

// Test with no base_path (stage case): relative to project root
let result = resolve_path(None, "data.txt", &input);
assert_eq!(result, PathBuf::from("/project/data.txt"));
}

#[test]
fn test_resolve_path_no_base_current_dir() {
let input = PathBuf::from("/project/stage.gbo");

// Test ./ prefix with no base_path: relative to stage directory
let result = resolve_path(None, "./data.txt", &input);
assert_eq!(result, PathBuf::from("/project/data.txt"));
}

#[test]
fn test_resolve_path_absolute() {
let base_path = PathBuf::from("/project/sprites/sprite.gbo");
let input = PathBuf::from("/project/stage.gbo");

// Test absolute path
let result = resolve_path(Some(&base_path), "/absolute/data.txt", &input);
assert_eq!(result, PathBuf::from("/absolute/data.txt"));
}
}
22 changes: 16 additions & 6 deletions src/codegen/sb3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,13 +869,23 @@ where T: Write + Seek
.into_iter()
.map(|v| s.evaluate_const_expr(d, v).to_string())
.collect(),
Some(ListDefault::File { path, span }) => match read_list(fs, input, path) {
Ok(data) => data,
Err(error) => {
d.report(error, span);
vec![]
Some(ListDefault::File { path, span }) => {
// Get the current file path from translation unit based on span
let current_file_path = if d.translation_unit.get_text().len() > span.start {
let (_, include) = d.translation_unit.translate_position(span.start);
&include.path
} else {
// Fallback to input path if span is invalid
input
};
match read_list(fs, input, path, &current_file_path) {
Ok(data) => data,
Err(error) => {
d.report(error, span);
vec![]
}
}
},
}
None => vec![],
};
match &list.type_ {
Expand Down
Loading