Phase 5: Projekt- und Dokumentmodell implementieren und archivieren

This commit is contained in:
2026-09-06 15:47:40 +02:00
parent 747ec34c6a
commit 687fc230ec
23 changed files with 2261 additions and 211 deletions

View File

@@ -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(&current)?.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) => {