mirror of
https://github.com/imjasonh/nescript
synced 2026-07-18 06:36:57 +00:00
Add include directive for multi-file projects
Parser/preprocess module: - New src/parser/preprocess.rs handles source-level include inlining - 'include "path"' at line start splices the target file's content - Paths resolved relative to the including file's directory - Circular include detection via visited set (canonicalized paths) - Non-cycle re-includes allowed (sibling branches can reuse files) main.rs: - Both compile() and check() run preprocess_source() before parsing - Include resolution failures produce a clear error message Tests: 4 new preprocess unit tests (249 total) - passthrough, include line parsing, temp-file roundtrip https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
18e9bed258
commit
6efef6c4c5
3 changed files with 140 additions and 2 deletions
13
src/main.rs
13
src/main.rs
|
|
@ -81,10 +81,15 @@ fn dump_asm(instructions: &[nescript::asm::Instruction]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile(input: &PathBuf, _debug: bool, asm_dump: bool) -> Result<Vec<u8>, ()> {
|
fn compile(input: &PathBuf, _debug: bool, asm_dump: bool) -> Result<Vec<u8>, ()> {
|
||||||
let source = std::fs::read_to_string(input).map_err(|e| {
|
let raw_source = std::fs::read_to_string(input).map_err(|e| {
|
||||||
eprintln!("error: failed to read {}: {e}", input.display());
|
eprintln!("error: failed to read {}: {e}", input.display());
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Preprocess: inline include directives
|
||||||
|
let source = nescript::parser::preprocess_source(&raw_source, Some(input)).map_err(|e| {
|
||||||
|
eprintln!("error: {e}");
|
||||||
|
})?;
|
||||||
|
|
||||||
let filename = input.to_string_lossy();
|
let filename = input.to_string_lossy();
|
||||||
|
|
||||||
// Parse
|
// Parse
|
||||||
|
|
@ -141,10 +146,14 @@ fn compile(input: &PathBuf, _debug: bool, asm_dump: bool) -> Result<Vec<u8>, ()>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check(input: &PathBuf) -> Result<(), ()> {
|
fn check(input: &PathBuf) -> Result<(), ()> {
|
||||||
let source = std::fs::read_to_string(input).map_err(|e| {
|
let raw_source = std::fs::read_to_string(input).map_err(|e| {
|
||||||
eprintln!("error: failed to read {}: {e}", input.display());
|
eprintln!("error: failed to read {}: {e}", input.display());
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let source = nescript::parser::preprocess_source(&raw_source, Some(input)).map_err(|e| {
|
||||||
|
eprintln!("error: {e}");
|
||||||
|
})?;
|
||||||
|
|
||||||
let filename = input.to_string_lossy();
|
let filename = input.to_string_lossy();
|
||||||
|
|
||||||
let (program, parse_diags) = nescript::parser::parse(&source);
|
let (program, parse_diags) = nescript::parser::parse(&source);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
pub mod ast;
|
pub mod ast;
|
||||||
|
pub mod preprocess;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
pub use preprocess::preprocess as preprocess_source;
|
||||||
|
|
||||||
use crate::errors::{Diagnostic, ErrorCode};
|
use crate::errors::{Diagnostic, ErrorCode};
|
||||||
use crate::lexer::{Span, Token, TokenKind};
|
use crate::lexer::{Span, Token, TokenKind};
|
||||||
use ast::*;
|
use ast::*;
|
||||||
|
|
|
||||||
126
src/parser/preprocess.rs
Normal file
126
src/parser/preprocess.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
//! Source-level include preprocessor.
|
||||||
|
//!
|
||||||
|
//! Before lexing, scan the source for `include "path"` directives at the
|
||||||
|
//! start of lines and splice in the contents of those files. Tracks an
|
||||||
|
//! include stack to detect circular includes.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Preprocess a source file, inlining all `include "path"` directives.
|
||||||
|
pub fn preprocess(source: &str, source_path: Option<&Path>) -> Result<String, String> {
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
let base_dir = source_path
|
||||||
|
.and_then(|p| p.parent())
|
||||||
|
.unwrap_or_else(|| Path::new("."))
|
||||||
|
.to_path_buf();
|
||||||
|
if let Some(p) = source_path {
|
||||||
|
if let Ok(canon) = p.canonicalize() {
|
||||||
|
visited.insert(canon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process(source, &base_dir, &mut visited)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(
|
||||||
|
source: &str,
|
||||||
|
base_dir: &Path,
|
||||||
|
visited: &mut HashSet<PathBuf>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let mut out = String::new();
|
||||||
|
for line in source.lines() {
|
||||||
|
if let Some(path_str) = parse_include_line(line) {
|
||||||
|
let include_path = resolve_include(base_dir, path_str);
|
||||||
|
let canon = include_path
|
||||||
|
.canonicalize()
|
||||||
|
.map_err(|e| format!("failed to open '{}': {e}", include_path.display()))?;
|
||||||
|
if !visited.insert(canon.clone()) {
|
||||||
|
return Err(format!(
|
||||||
|
"circular include: '{}' is already in the include stack",
|
||||||
|
include_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let included = std::fs::read_to_string(&canon)
|
||||||
|
.map_err(|e| format!("failed to read '{}': {e}", canon.display()))?;
|
||||||
|
// Use the included file's directory as base for its own includes
|
||||||
|
let nested_base = canon
|
||||||
|
.parent()
|
||||||
|
.map_or_else(|| base_dir.to_path_buf(), std::path::Path::to_path_buf);
|
||||||
|
let processed = process(&included, &nested_base, visited)?;
|
||||||
|
out.push_str(&processed);
|
||||||
|
out.push('\n');
|
||||||
|
// Remove from visited so siblings can re-include (only cycles
|
||||||
|
// through the current branch are forbidden).
|
||||||
|
visited.remove(&canon);
|
||||||
|
} else {
|
||||||
|
out.push_str(line);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a line like `include "path/to/file.ne"` and extract the path.
|
||||||
|
/// Returns None if the line is not an include directive.
|
||||||
|
fn parse_include_line(line: &str) -> Option<&str> {
|
||||||
|
let trimmed = line.trim_start();
|
||||||
|
let rest = trimmed.strip_prefix("include")?;
|
||||||
|
let rest = rest.trim_start();
|
||||||
|
let rest = rest.strip_prefix('"')?;
|
||||||
|
let end = rest.find('"')?;
|
||||||
|
Some(&rest[..end])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_include(base_dir: &Path, path_str: &str) -> PathBuf {
|
||||||
|
let p = Path::new(path_str);
|
||||||
|
if p.is_absolute() {
|
||||||
|
p.to_path_buf()
|
||||||
|
} else {
|
||||||
|
base_dir.join(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preprocess_no_includes_passthrough() {
|
||||||
|
let src = "game \"T\" { mapper: NROM }\non frame { wait_frame }\nstart Main\n";
|
||||||
|
let out = preprocess(src, None).unwrap();
|
||||||
|
assert!(out.contains("game \"T\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_include_line_valid() {
|
||||||
|
assert_eq!(parse_include_line("include \"foo.ne\""), Some("foo.ne"));
|
||||||
|
assert_eq!(parse_include_line(" include \"a/b.ne\""), Some("a/b.ne"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_include_line_invalid() {
|
||||||
|
assert_eq!(parse_include_line("var x: u8 = 0"), None);
|
||||||
|
assert_eq!(parse_include_line("include foo.ne"), None); // no quotes
|
||||||
|
assert_eq!(parse_include_line("// include \"foo.ne\""), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preprocess_with_temp_file() {
|
||||||
|
use std::io::Write;
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let inc_path = dir.join("nescript_test_include.ne");
|
||||||
|
let main_path = dir.join("nescript_test_main.ne");
|
||||||
|
|
||||||
|
std::fs::write(&inc_path, "const SPEED: u8 = 5\n").unwrap();
|
||||||
|
let main_src = format!("include \"{}\"\nvar x: u8 = 0\n", inc_path.display());
|
||||||
|
let mut f = std::fs::File::create(&main_path).unwrap();
|
||||||
|
f.write_all(main_src.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
let out = preprocess(&main_src, Some(&main_path)).unwrap();
|
||||||
|
assert!(out.contains("const SPEED"));
|
||||||
|
assert!(out.contains("var x: u8"));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&inc_path);
|
||||||
|
let _ = std::fs::remove_file(&main_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue