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:
+8
-1
@@ -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,
|
||||||
|
|||||||
+88
-46
@@ -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 {
|
ctx.with_span(label.span.clone())
|
||||||
let section = make_section_id("");
|
.error("local label definitions cannot specify a section name");
|
||||||
namespace_to_section.insert(label.namespace.clone(), section.clone());
|
|
||||||
section
|
|
||||||
}
|
}
|
||||||
} else if label.local.is_none() {
|
} else if is_definition {
|
||||||
// Interpret label :name as start of new section.
|
// Top-level label definition, such as ":touch"
|
||||||
// Only label definitions do this; label references have no effect.
|
resolver.start_new_section(&label.name);
|
||||||
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())
|
|
||||||
.error("local labels with section names not supported");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user