Implement new algorithm for local label processing

This change allows qualified references to local labels from other
sections: you can do `#send section.local` and it will take you to the
appropriate `:.local` label. When `section.local` is ambiguous due to
sections/locals sharing the same names, the reference resolves to the
local in the first such section.
This commit is contained in:
2025-07-10 22:36:31 -07:00
parent d751aade99
commit 3bddc3c85e
7 changed files with 165 additions and 58 deletions
+8 -1
View File
@@ -75,6 +75,13 @@ impl<'a> Context<'a> {
}); });
} }
pub fn any_errors(&self) -> bool {
match self {
Context::Base(refcell) => refcell.borrow().iter().any(|x| x.level == Level::Error),
Context::With(parent, _info) => parent.any_errors(),
}
}
pub fn into_messages(self) -> Vec<CompileMessage> { pub fn into_messages(self) -> Vec<CompileMessage> {
match self { match self {
Context::Base(refcell) => { Context::Base(refcell) => {
@@ -102,7 +109,7 @@ pub struct CompileMessage {
pub location: Location, pub location: Location,
} }
#[derive(Debug)] #[derive(Debug, Eq, PartialEq)]
pub enum Level { pub enum Level {
Error, Error,
Warning, Warning,
+87 -45
View File
@@ -8,7 +8,7 @@ use super::{
sanitize::Registry, sanitize::Registry,
}; };
pub fn process_labels(board: &Board, ctx: &Context) -> Board { pub fn process_labels(board: &Board, ctx: &Context) -> Option<Board> {
let mut board = board.clone(); let mut board = board.clone();
// Parse stats into chunks // Parse stats into chunks
@@ -20,10 +20,17 @@ pub fn process_labels(board: &Board, ctx: &Context) -> Board {
.collect(); .collect();
let mut registry = Registry::new(); let mut registry = Registry::new();
// Expand ".local" labels to full "section.local" form.
resolve_local_labels(&mut stats, ctx); resolve_local_labels(&mut stats, ctx);
assign_named_labels(&mut stats, &mut registry);
// Assign names to anonymous labels // Sanitize all non-anonymous labels.
// This condenses name strings like "namespace~name$1.local" down to
// something short and valid for ZZT-OOP, e.g., "local_".
sanitize_named_labels(&mut stats, &mut registry);
// Replace anonymous labels with short names.
// This happens after sanitization so we know which short names are available to use.
// Two passes are needed because anonymous references can point either forward or backward.
anonymous_forward_pass(&mut stats, &mut registry, ctx); anonymous_forward_pass(&mut stats, &mut registry, ctx);
anonymous_backward_pass(&mut stats, ctx); anonymous_backward_pass(&mut stats, ctx);
@@ -43,7 +50,7 @@ pub fn process_labels(board: &Board, ctx: &Context) -> Board {
old_stat.code = new_code; old_stat.code = new_code;
} }
board (!ctx.any_errors()).then_some(board)
} }
/// Resolve ".local" labels to "name.local" form. /// Resolve ".local" labels to "name.local" form.
@@ -51,15 +58,8 @@ fn resolve_local_labels(stats: &mut [ParsedStat], ctx: &Context) {
for (i, stat) in stats.iter_mut().enumerate() { for (i, stat) in stats.iter_mut().enumerate() {
let ctx = ctx.with_stat(i); let ctx = ctx.with_stat(i);
// Helper: Generate unique section strings like "touch$0" // Keep a separate resolver for each namespace
let mut i = 0; let mut resolvers: FxHashMap<Option<CompactString>, LocalLabelResolver> =
let mut make_section_id = |label_name: &str| -> CompactString {
let result = format!("{}${}", &label_name, i);
i += 1;
result.into()
};
let mut namespace_to_section: FxHashMap<Option<CompactString>, CompactString> =
FxHashMap::default(); FxHashMap::default();
for chunk in stat.iter_mut() { for chunk in stat.iter_mut() {
@@ -69,33 +69,22 @@ fn resolve_local_labels(stats: &mut [ParsedStat], ctx: &Context) {
is_anon: false, is_anon: false,
name: label, name: label,
} => { } => {
// Handle cases where either one of (section name, label) is missing. let resolver = resolvers.entry(label.namespace.clone()).or_default();
// If both are present ("name.local") then the label is already fully resolved let is_definition = !*is_ref;
// and there is nothing to do. if let Some(local) = &label.local {
if label.name.is_empty() { if label.name.is_empty() {
// Expand :.local to :name.local // Local that needs to be resolved, such as ":.foo" or "#send .foo"
assert!(label.local.is_some()); label.name = resolver.get_section_prefix(&local)
label.name = if let Some(section) = } else if is_definition {
namespace_to_section.get(&label.namespace) // Illegal local label definition, such as ":touch.foo"
{ // _References_ to local labels may specify a section name: "#send touch.foo".
section.clone() // But when a local is _defined_, the section name must always be inferred.
} else {
let section = make_section_id("");
namespace_to_section.insert(label.namespace.clone(), section.clone());
section
}
} else if label.local.is_none() {
// Interpret label :name as start of new section.
// Only label definitions do this; label references have no effect.
if !*is_ref {
namespace_to_section
.insert(label.namespace.clone(), make_section_id(&label.name));
}
} else {
// User specified both a section name and a local name ("name.local").
// This isn't allowed because of issues around resolving the section ID.
ctx.with_span(label.span.clone()) ctx.with_span(label.span.clone())
.error("local labels with section names not supported"); .error("local label definitions cannot specify a section name");
}
} else if is_definition {
// Top-level label definition, such as ":touch"
resolver.start_new_section(&label.name);
} }
} }
_ => {} _ => {}
@@ -104,8 +93,59 @@ fn resolve_local_labels(stats: &mut [ParsedStat], ctx: &Context) {
} }
} }
#[derive(Default)]
struct LocalLabelResolver {
current_section: CompactString,
current_section_index: usize,
pair_info: FxHashMap<(CompactString, CompactString), LocalLabelInfo>,
}
struct LocalLabelInfo {
last_section_index: usize,
num_sections: usize,
}
impl<'a> LocalLabelResolver {
/// Record the start of a new section, e.g., `touch`.
///
/// This is called for each occurence of a top-level label; if the same
/// label name appears multiple times, that creates multiple sections.
fn start_new_section(&mut self, section: &str) {
self.current_section = section.into();
self.current_section_index += 1;
}
/// Generate a distinct section string (e.g., `touch$1`) for a local label.
///
/// The output depends not just on the section's and local's names, but also
/// the number of previous sections that have had that section-local combo:
///
/// - The first time `.foo` appears in a `touch` section, all instances of
/// `.foo` within that section resolve to `touch.foo`.
/// - But if there's a _second_ `touch` label, any instances of `.foo` within
/// _that_ section must resolve to a distinct name: `touch$1.foo`.
fn get_section_prefix(&mut self, local: &str) -> CompactString {
let key = (self.current_section.clone(), local.into());
let info = self.pair_info.entry(key).or_insert_with(|| LocalLabelInfo {
last_section_index: self.current_section_index,
num_sections: 1,
});
if info.last_section_index != self.current_section_index {
info.last_section_index = self.current_section_index;
info.num_sections += 1;
}
let mut result = self.current_section.clone();
if info.num_sections > 1 {
let disambiguator = info.num_sections - 1;
result.push('$');
result.push_str(&disambiguator.to_string());
}
result
}
}
/// Assign sanitized names to all of the named labels. /// Assign sanitized names to all of the named labels.
fn assign_named_labels(stats: &mut [ParsedStat], registry: &mut Registry) { fn sanitize_named_labels(stats: &mut [ParsedStat], registry: &mut Registry) {
for stat in stats.iter_mut() { for stat in stats.iter_mut() {
for chunk in stat.iter_mut() { for chunk in stat.iter_mut() {
match chunk { match chunk {
@@ -140,6 +180,8 @@ fn anonymous_forward_pass(stats: &mut [ParsedStat], registry: &mut Registry, ctx
let mut label_names = vec![]; let mut label_names = vec![];
for (stat_index, stat) in stats.iter_mut().enumerate() { for (stat_index, stat) in stats.iter_mut().enumerate() {
let ctx = ctx.with_stat(stat_index);
// Helper: Get the next label name that hasn't been used in this object yet // Helper: Get the next label name that hasn't been used in this object yet
let mut i = 0; let mut i = 0;
let mut get_next_name = || -> CompactString { let mut get_next_name = || -> CompactString {
@@ -155,7 +197,6 @@ fn anonymous_forward_pass(stats: &mut [ParsedStat], registry: &mut Registry, ctx
let mut namespace_to_latest: FxHashMap<Option<CompactString>, CompactString> = let mut namespace_to_latest: FxHashMap<Option<CompactString>, CompactString> =
FxHashMap::default(); FxHashMap::default();
let ctx = ctx.with_stat(stat_index);
for chunk in stat.iter_mut() { for chunk in stat.iter_mut() {
match chunk { match chunk {
Chunk::Label { Chunk::Label {
@@ -190,8 +231,9 @@ fn anonymous_forward_pass(stats: &mut [ParsedStat], registry: &mut Registry, ctx
/// Resolve anonymous forward references to their label names. /// Resolve anonymous forward references to their label names.
fn anonymous_backward_pass(stats: &mut [ParsedStat], ctx: &Context) { fn anonymous_backward_pass(stats: &mut [ParsedStat], ctx: &Context) {
for (stat_index, stat) in stats.iter_mut().enumerate() { for (stat_index, stat) in stats.iter_mut().enumerate() {
let mut namespace_to_latest = FxHashMap::default();
let ctx = ctx.with_stat(stat_index); let ctx = ctx.with_stat(stat_index);
let mut namespace_to_latest = FxHashMap::default();
for chunk in stat.iter_mut().rev() { for chunk in stat.iter_mut().rev() {
match chunk { match chunk {
Chunk::Label { Chunk::Label {
@@ -276,28 +318,28 @@ mod test {
#[test] #[test]
fn test_label_sanitization() { fn test_label_sanitization() {
let board = board_from_text("tests/labels/sanitize.txt"); let board = board_from_text("tests/labels/sanitize.txt");
let board = process_labels(&board, &Context::new()); let board = process_labels(&board, &Context::new()).unwrap();
assert_snapshot!(board_to_text(board)); assert_snapshot!(board_to_text(board));
} }
#[test] #[test]
fn test_anonymous_labels() { fn test_anonymous_labels() {
let board = board_from_text("tests/labels/anonymous.txt"); let board = board_from_text("tests/labels/anonymous.txt");
let board = process_labels(&board, &Context::new()); let board = process_labels(&board, &Context::new()).unwrap();
assert_snapshot!(board_to_text(board)); assert_snapshot!(board_to_text(board));
} }
#[test] #[test]
fn test_local_labels() { fn test_local_labels() {
let board = board_from_text("tests/labels/local.txt"); let board = board_from_text("tests/labels/local.txt");
let board = process_labels(&board, &Context::new()); let board = process_labels(&board, &Context::new()).unwrap();
assert_snapshot!(board_to_text(board)); assert_snapshot!(board_to_text(board));
} }
#[test] #[test]
fn test_namespaces() { fn test_namespaces() {
let board = board_from_text("tests/labels/namespaces.txt"); let board = board_from_text("tests/labels/namespaces.txt");
let board = process_labels(&board, &Context::new()); let board = process_labels(&board, &Context::new()).unwrap();
assert_snapshot!(board_to_text(board)); assert_snapshot!(board_to_text(board));
} }
@@ -82,12 +82,11 @@ error: message targets not supported for local labels
7 | :.local 7 | :.local
| |
error: local labels with section names not supported error: local label definitions cannot specify a section name
=> test.zzt -> Title screen -> @Locals with section names (1,1) -> line 3:2 => test.zzt -> Title screen -> @Local label definitions with section names (1,1) -> line 8:2
| |
1 | @Locals with section names 5 | #touch.skip
2 | :touch 6 | '
3 | #touch.skip 7 | 'this is not
8 | :touch.skip
| ^^^^^^^^^^ | ^^^^^^^^^^
4 | :.skip
|
@@ -53,3 +53,30 @@ Trying to sell you some torches...
#zap _z #zap _z
#_z #_z
:_z :_z
---
'Addressing labels with section.local
:touch
:_foo
#end
:touch
:_foo0
#end
:shot
'sends to the first one:
#_foo
---
@alice
:shared
'this is the 1st section named "shared"
'but its local should correspond with @bob's 2nd "shared"
:_local
#end
:shared
---
@bob
:shared
#end
:shared
'this is the 2nd section named "shared"
'but its local should correspond with @alice's 1st "shared"
:_local
+3 -1
View File
@@ -84,7 +84,9 @@ fn main() -> Result<()> {
let ctx = base_ctx.with_file_path(&input_filename); let ctx = base_ctx.with_file_path(&input_filename);
for (i, board) in world.boards.iter_mut().enumerate() { for (i, board) in world.boards.iter_mut().enumerate() {
let ctx = ctx.with_board(i); let ctx = ctx.with_board(i);
*board = process_labels(&board, &ctx); if let Some(processed_board) = process_labels(&board, &ctx) {
*board = processed_board;
}
} }
// Print diagnostics // Print diagnostics
+6 -2
View File
@@ -23,7 +23,11 @@
:@ :@
:.local :.local
--- ---
@Locals with section names @Local label definitions with section names
'these are okay
:touch :touch
#touch.skip
:.skip :.skip
#touch.skip
'
'this is not
:touch.skip
+27 -1
View File
@@ -49,4 +49,30 @@ Trying to sell you some torches...
#zap .z #zap .z
#.z #.z
:.z :.z
---
'Addressing labels with section.local
:touch
:.foo
#end
:touch
:.foo
#end
:shot
'sends to the first one:
#touch.foo
---
@alice
:shared
'this is the 1st section named "shared"
'but its local should correspond with @bob's 2nd "shared"
:.local
#end
:shared
---
@bob
:shared
#end
:shared
'this is the 2nd section named "shared"
'but its local should correspond with @alice's 1st "shared"
:.local