Phase 5: Projekt- und Dokumentmodell implementieren und archivieren
This commit is contained in:
@@ -15,6 +15,7 @@ use std::process::ExitCode;
|
||||
use tb_runtime::host::{Ereignis, Host};
|
||||
use tb_ui::host::TerminalHost;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
use tb_vm::project_io::{module_name, relative_case_insensitive, SourceLoader};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
@@ -75,168 +76,6 @@ fn cmd_convert_frm(args: &[String]) -> ExitCode {
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn module_name(path: &Path) -> String {
|
||||
path.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_uppercase())
|
||||
.unwrap_or_else(|| "MODUL".into())
|
||||
}
|
||||
|
||||
fn relative_case_insensitive(base: &Path, relative: &str) -> std::io::Result<PathBuf> {
|
||||
let direct = base.join(relative);
|
||||
if direct.exists() {
|
||||
return Ok(direct);
|
||||
}
|
||||
let wanted = Path::new(relative);
|
||||
let mut current = base.to_path_buf();
|
||||
for component in wanted.components() {
|
||||
use std::path::Component;
|
||||
match component {
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
current.pop();
|
||||
}
|
||||
Component::Normal(name) => {
|
||||
let entry = std::fs::read_dir(¤t)?.find_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.eq_ignore_ascii_case(&name.to_string_lossy())
|
||||
.then(|| entry.path())
|
||||
});
|
||||
current = entry.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("{} nicht gefunden", current.join(name).display()),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Component::RootDir | Component::Prefix(_) => current.push(component.as_os_str()),
|
||||
}
|
||||
}
|
||||
Ok(current)
|
||||
}
|
||||
|
||||
fn include_name(line: &str) -> Option<String> {
|
||||
let trimmed = line.trim();
|
||||
let upper = trimmed.to_ascii_uppercase();
|
||||
let at = upper.find("$INCLUDE")?;
|
||||
let rest = trimmed[at + "$INCLUDE".len()..].trim_start();
|
||||
let rest = rest.strip_prefix(':')?.trim_start();
|
||||
let rest = rest.strip_prefix('\'')?;
|
||||
Some(rest.split('\'').next().unwrap_or_default().to_string())
|
||||
}
|
||||
|
||||
fn expand_includes(
|
||||
path: &Path,
|
||||
source: &str,
|
||||
first_line: u32,
|
||||
stack: &mut Vec<PathBuf>,
|
||||
) -> Result<Vec<tb_frontend::source::SourceSegment>, String> {
|
||||
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
if stack.contains(&canonical) {
|
||||
return Err(format!("{}: zyklisches $INCLUDE", path.display()));
|
||||
}
|
||||
stack.push(canonical);
|
||||
let mut segments = vec![tb_frontend::source::SourceSegment {
|
||||
file: path.display().to_string(),
|
||||
first_line,
|
||||
text: String::new(),
|
||||
}];
|
||||
for (line_no, line) in source.split_inclusive('\n').enumerate() {
|
||||
if let Some(name) = include_name(line) {
|
||||
if name.is_empty() {
|
||||
return Err(format!(
|
||||
"{}:{}: leeres $INCLUDE",
|
||||
path.display(),
|
||||
first_line + line_no as u32
|
||||
));
|
||||
}
|
||||
let included =
|
||||
relative_case_insensitive(path.parent().unwrap_or(Path::new(".")), &name)
|
||||
.map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
let text = std::fs::read_to_string(&included)
|
||||
.map_err(|e| format!("{}: {e}", included.display()))?;
|
||||
segments.extend(expand_includes(&included, &text, 1, stack)?);
|
||||
} else {
|
||||
segments.push(tb_frontend::source::SourceSegment {
|
||||
file: path.display().to_string(),
|
||||
first_line: first_line + line_no as u32,
|
||||
text: line.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
stack.pop();
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
|
||||
let bytes = std::fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?;
|
||||
if bytes.starts_with(b"VERSION ") || bytes.starts_with(b"Version ") {
|
||||
let source = std::str::from_utf8(&bytes)
|
||||
.map_err(|_| format!("{}: ungültige Textkodierung", path.display()))?;
|
||||
let form = tb_ui::frm::read_text(&path.display().to_string(), source)
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(form)
|
||||
} else {
|
||||
tb_ui::frm::read_binary(&path.display().to_string(), &bytes)
|
||||
.map(|read| read.form)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn input_sources(
|
||||
path: &Path,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<tb_frontend::source::SourceUnit>,
|
||||
Vec<tb_ui::frm::FormFile>,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
||||
let paths = if extension.eq_ignore_ascii_case("mak") {
|
||||
let project = std::fs::read_to_string(path)
|
||||
.map_err(|error| format!("{}: {error}", path.display()))?;
|
||||
let base = path.parent().unwrap_or(Path::new("."));
|
||||
project
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('\''))
|
||||
.map(|line| {
|
||||
relative_case_insensitive(base, line)
|
||||
.map_err(|error| format!("{}: {error}", path.display()))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
} else {
|
||||
vec![path.to_path_buf()]
|
||||
};
|
||||
let mut source = Vec::new();
|
||||
let mut forms = Vec::new();
|
||||
for member in paths {
|
||||
if member
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
|
||||
{
|
||||
let form = read_form(&member)?;
|
||||
source.push(tb_frontend::source::SourceUnit {
|
||||
name: form.root.name.clone(),
|
||||
segments: expand_includes(&member, &form.code, form.code_line(), &mut Vec::new())?,
|
||||
});
|
||||
forms.push(form);
|
||||
} else {
|
||||
let text = std::fs::read_to_string(&member)
|
||||
.map_err(|error| format!("{}: {error}", member.display()))?;
|
||||
source.push(tb_frontend::source::SourceUnit {
|
||||
name: module_name(&member),
|
||||
segments: expand_includes(&member, &text, 1, &mut Vec::new())?,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok((source, forms))
|
||||
}
|
||||
|
||||
fn compile(
|
||||
path_arg: Option<&String>,
|
||||
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||
@@ -259,7 +98,7 @@ fn compile(
|
||||
})?;
|
||||
return Ok((path, module));
|
||||
}
|
||||
let (source, forms) = match input_sources(&path) {
|
||||
let input = match SourceLoader::default().load(&path) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
@@ -267,10 +106,11 @@ fn compile(
|
||||
}
|
||||
};
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
for form in &forms {
|
||||
for form in &input.forms {
|
||||
catalog.append(&form.catalog());
|
||||
}
|
||||
let compiled = tb_vm::compile_project(&module_name(&path), &source, &catalog, &forms);
|
||||
let compiled =
|
||||
tb_vm::compile_project(&module_name(&path), &input.units, &catalog, &input.forms);
|
||||
match compiled {
|
||||
Ok(m) => Ok((path, m)),
|
||||
Err(diags) => {
|
||||
|
||||
Reference in New Issue
Block a user