Compare commits
9 Commits
3afc112810
..
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| d699340ec8 | |||
| e8fa45fc3b | |||
| 84f35c8b44 | |||
| 85ba7b63c8 | |||
| f6df3222ab | |||
| 8817cd8e2e | |||
| 3bddc3c85e | |||
| d751aade99 | |||
| 051e08fba9 |
Generated
+19
-26
@@ -10,9 +10,9 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "castaway"
|
name = "castaway"
|
||||||
version = "0.2.3"
|
version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
@@ -138,8 +138,8 @@ dependencies = [
|
|||||||
"compact_str",
|
"compact_str",
|
||||||
"insta",
|
"insta",
|
||||||
"lexopt",
|
"lexopt",
|
||||||
|
"mzp-peg-macro",
|
||||||
"nom",
|
"nom",
|
||||||
"peg_macro",
|
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -150,29 +150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "mzp-peg-macro"
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "7.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"minimal-lexical",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.21.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "peg_macro"
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itertools",
|
"itertools",
|
||||||
@@ -181,6 +159,21 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.95"
|
version = "1.0.95"
|
||||||
|
|||||||
+8
-8
@@ -1,16 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "marzipan"
|
name = "marzipan"
|
||||||
|
description = "A macro processor for ZZT worlds"
|
||||||
|
keywords = ["zzt", "gamedev"]
|
||||||
|
categories = ["compilers", "command-line-utilities"]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/cmounce/marzipan"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
codepage-437 = "0.1.0"
|
codepage-437 = "0.1.0"
|
||||||
peg_macro = { path = "./peg_macro"}
|
mzp-peg-macro = { path = "./mzp_peg_macro", version = "0.1.0" }
|
||||||
insta = "1.39.0"
|
nom = "8.0.0"
|
||||||
nom = "7.1.3"
|
|
||||||
compact_str = "0.9.0"
|
compact_str = "0.9.0"
|
||||||
rustc-hash = "2.1.1"
|
rustc-hash = "2.1.1"
|
||||||
lexopt = "0.3.1"
|
lexopt = "0.3.1"
|
||||||
@@ -22,9 +26,5 @@ insta = "1.39.0"
|
|||||||
insta.opt-level = 3
|
insta.opt-level = 3
|
||||||
similar.opt-level = 3
|
similar.opt-level = 3
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "marzipan"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "peg_macro"]
|
members = [".", "mzp_peg_macro"]
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Chris Mounce
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Marzipan
|
||||||
|
Marzipan is a macro processor for [ZZT](https://en.wikipedia.org/wiki/ZZT).
|
||||||
|
It extends the ZZT-OOP scripting language while still maintaining compatibility with ZZT 3.2: all language features compile down to vanilla ZZT-OOP.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Anonymous labels:** Like assembly code, ZZT-OOP control flow relies heavily on gotos.
|
||||||
|
Anonymous labels reduce the cognitive overhead of writing such code.
|
||||||
|
`:@` defines a label with a unique name, and `@f`/`@b` reference the nearest anonymous label forward/backward of the current line.
|
||||||
|
- **Local labels:** Label names with a dot are scoped to a single section of an object's program.
|
||||||
|
This allows you to reuse a name like `.loop` multiple times in a single object's code.
|
||||||
|
Marzipan will replace this with a distinct name per section: `loop_` in one section, `loopa` in the next section, etc.
|
||||||
|
- **Macro language (WIP):** Lines starting with `%` invoke a Marzipan macro.
|
||||||
|
Macros work by text substitution; for example, `%include "foo.txt"` will insert the contents of a text file at the current line.
|
||||||
|
|
||||||
|
Here's an example of what anonymous labels look like in practice:
|
||||||
|
|
||||||
|
```
|
||||||
|
@Treasure chest
|
||||||
|
#end
|
||||||
|
:touch 'Event handler, user touched the chest.
|
||||||
|
#if key @f 'Jump to anonymous label. Marzipan compiles `@f` to `_`...
|
||||||
|
The chest is locked.
|
||||||
|
#end
|
||||||
|
:@ '...and compiles this to `:_`.
|
||||||
|
You unlock the chest.
|
||||||
|
There's a bunch of gems inside!
|
||||||
|
#give gems 20
|
||||||
|
#die
|
||||||
|
|
||||||
|
:shot 'Event handler, user shot at the chest.
|
||||||
|
#if shotgun @f 'This time, Marzipan compiles `@f` to `a`...
|
||||||
|
Nothing happens.
|
||||||
|
#end
|
||||||
|
:@ '...and compiles this to `:a`.
|
||||||
|
The chest shatters!
|
||||||
|
Gems fly everywhere.
|
||||||
|
#put w green gem
|
||||||
|
#put e green gem
|
||||||
|
#become green gem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Marzipan reads and writes ZZT world files, a binary file format.
|
||||||
|
You will need either ZZT itself or an external ZZT editor (such as [KevEdit](https://github.com/cknave/kevedit)) to work with them.
|
||||||
|
|
||||||
|
```
|
||||||
|
marzipan WORLD.ZZT -o ./dest_folder/WORLD.ZZT
|
||||||
|
```
|
||||||
|
|
||||||
|
A disclaimer: **Marzipan is experimental.**
|
||||||
|
It hasn't eaten my code yet, but I cannot guarantee it will treat your code with kindness.
|
||||||
|
If you use it, make sure to keep backups of your work. (You were already keeping backups, right?)
|
||||||
|
|
||||||
|
## Planned features
|
||||||
|
- Extending the macro system to a full language with variables, custom macro definitions, etc.
|
||||||
|
- Code minification, for generated code that bumps up against ZZT 3.2's size limits.
|
||||||
|
- Lints for ZZT-OOP, such as dead code analysis.
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "peg_macro"
|
name = "mzp-peg-macro"
|
||||||
|
description = "Internal crate for generating Marzipan's PEG parser code"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/cmounce/marzipan"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
A PEG parser generator built for [Marzipan](https://github.com/cmounce/marzipan).
|
||||||
|
|
||||||
|
Currently tightly coupled to Marzipan's specific needs and not designed for general use.
|
||||||
|
It might evolve into a standalone library in the future if there's broader interest.
|
||||||
|
But until then, you probably want something like [pest](https://pest.rs/) if you're looking for a PEG parser.
|
||||||
+8
-3
@@ -1,7 +1,8 @@
|
|||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use codepage_437::CP437_WINGDINGS;
|
use codepage_437::CP437_WINGDINGS;
|
||||||
|
|
||||||
// Serialize multi-line content with CR-terminated lines
|
/// Serialize a multi-line code string for an object or a scroll.
|
||||||
|
/// Uses ZZT's convention of CR-terminated lines.
|
||||||
pub fn encode_multiline(input: &str) -> Result<Vec<u8>> {
|
pub fn encode_multiline(input: &str) -> Result<Vec<u8>> {
|
||||||
input
|
input
|
||||||
.chars()
|
.chars()
|
||||||
@@ -17,6 +18,7 @@ pub fn encode_multiline(input: &str) -> Result<Vec<u8>> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserialize a multi-line code string.
|
||||||
pub fn decode_multiline(input: &[u8]) -> String {
|
pub fn decode_multiline(input: &[u8]) -> String {
|
||||||
input
|
input
|
||||||
.iter()
|
.iter()
|
||||||
@@ -30,6 +32,8 @@ pub fn decode_multiline(input: &[u8]) -> String {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize a single-line string for a board title.
|
||||||
|
/// Newlines cannot be encoded because ZZT interprets them as dingbats (♪).
|
||||||
pub fn encode_oneline(input: &str) -> Result<Vec<u8>> {
|
pub fn encode_oneline(input: &str) -> Result<Vec<u8>> {
|
||||||
input
|
input
|
||||||
.chars()
|
.chars()
|
||||||
@@ -41,6 +45,7 @@ pub fn encode_oneline(input: &str) -> Result<Vec<u8>> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserialize a board title.
|
||||||
pub fn decode_oneline(input: &[u8]) -> String {
|
pub fn decode_oneline(input: &[u8]) -> String {
|
||||||
input.iter().map(|&x| CP437_WINGDINGS.decode(x)).collect()
|
input.iter().map(|&x| CP437_WINGDINGS.decode(x)).collect()
|
||||||
}
|
}
|
||||||
@@ -86,12 +91,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn byte_13_to_wingding() {
|
fn byte_13_to_dingbat() {
|
||||||
// Code is allowed to have newlines
|
// Code is allowed to have newlines
|
||||||
let bytes = serialize_code("ABC\nDEF");
|
let bytes = serialize_code("ABC\nDEF");
|
||||||
assert!(bytes.contains(&13));
|
assert!(bytes.contains(&13));
|
||||||
|
|
||||||
// But in a board title, that same byte is a wingding
|
// But in a board title, that same byte is a dingbat
|
||||||
assert_debug_snapshot!(decode_oneline(&bytes), @r#""ABC♪DEF""#);
|
assert_debug_snapshot!(decode_oneline(&bytes), @r#""ABC♪DEF""#);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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,
|
||||||
|
|||||||
+1
-1
@@ -121,7 +121,7 @@ pub fn parse_stat_labels(stat: &Stat, ctx: &Context) -> ParsedStat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod grammar {
|
mod grammar {
|
||||||
use peg_macro::grammar;
|
use mzp_peg_macro::grammar;
|
||||||
|
|
||||||
grammar! {
|
grammar! {
|
||||||
program = (line ("\n" line)*)? EOI;
|
program = (line ("\n" line)*)? EOI;
|
||||||
|
|||||||
+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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+108
-147
@@ -1,187 +1,151 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
use compact_str::CompactString;
|
use compact_str::CompactString;
|
||||||
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Hash)]
|
pub struct Registry {
|
||||||
struct LabelId(CompactString);
|
key_to_suffix: FxHashMap<Lowercase, Suffix>,
|
||||||
|
names: FxHashSet<Lowercase>,
|
||||||
|
anonymous_counter: CompactString,
|
||||||
|
}
|
||||||
|
|
||||||
impl LabelId {
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
fn new(label: &str) -> Self {
|
struct Lowercase(CompactString);
|
||||||
let mut result = CompactString::new(label);
|
|
||||||
|
impl Lowercase {
|
||||||
|
fn new(key: &str) -> Self {
|
||||||
|
let mut result = CompactString::new(key);
|
||||||
result.make_ascii_lowercase();
|
result.make_ascii_lowercase();
|
||||||
Self(result)
|
Self(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Registry {
|
#[derive(Default)]
|
||||||
label_transforms: HashMap<LabelId, Transform>,
|
struct Suffix(CompactString);
|
||||||
existing: HashSet<CompactString>,
|
|
||||||
anonymous_counter: CompactString,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Transform {
|
impl Suffix {
|
||||||
Preferred,
|
fn apply(&self, key: &str) -> CompactString {
|
||||||
FilteredWithSuffix(CompactString),
|
let mut result = preferred_label_name(key);
|
||||||
}
|
result.push_str(&self.0);
|
||||||
|
result
|
||||||
impl Transform {
|
|
||||||
fn apply(&self, label: &str) -> CompactString {
|
|
||||||
let preferred = preferred_label_name(label);
|
|
||||||
match self {
|
|
||||||
Transform::Preferred => preferred,
|
|
||||||
Transform::FilteredWithSuffix(suffix) => {
|
|
||||||
let mut result = preferred;
|
|
||||||
result = Registry::filter_label_chars(&result);
|
|
||||||
result.push_str(&suffix);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Registry {
|
impl Registry {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let builtin_labels = ["bombed", "energize", "shot", "thud", "touch"];
|
let builtin_labels = ["bombed", "energize", "shot", "thud", "touch"];
|
||||||
let mut label_transforms = HashMap::new();
|
let mut key_to_suffix = FxHashMap::default();
|
||||||
let mut existing = HashSet::new();
|
let mut taken = FxHashSet::default();
|
||||||
for name in builtin_labels {
|
for name in builtin_labels {
|
||||||
let cs = CompactString::new(name);
|
let name = Lowercase(name.into());
|
||||||
label_transforms.insert(LabelId(cs.clone()), Transform::Preferred);
|
key_to_suffix.insert(name.clone(), Suffix::default());
|
||||||
existing.insert(cs);
|
taken.insert(name);
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
label_transforms,
|
key_to_suffix,
|
||||||
existing,
|
names: taken,
|
||||||
anonymous_counter: "_".into(),
|
anonymous_counter: CompactString::const_new(""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sanitize(&mut self, label: &str) -> CompactString {
|
pub fn sanitize(&mut self, key: &str) -> CompactString {
|
||||||
// Use existing sanitization if one exists
|
// Use existing sanitization if one exists
|
||||||
let id = LabelId::new(label);
|
let key_lower = Lowercase::new(key);
|
||||||
if let Some(transform) = self.label_transforms.get(&id) {
|
if let Some(transform) = self.key_to_suffix.get(&key_lower) {
|
||||||
return transform.apply(label);
|
return transform.apply(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to use label's preferred name as-is
|
// Append suffixes until we find a name that's not taken yet
|
||||||
let preferred = Transform::Preferred.apply(label);
|
let mut candidate = Lowercase::new(&preferred_label_name(key));
|
||||||
if Registry::is_valid_label(&preferred) {
|
let mut suffix = CompactString::const_new("");
|
||||||
let key = preferred.to_ascii_lowercase();
|
let original_len = candidate.0.len();
|
||||||
if !self.existing.contains(&key) {
|
|
||||||
self.label_transforms.insert(id, Transform::Preferred);
|
|
||||||
self.existing.insert(key);
|
|
||||||
return preferred;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a new name by appending suffixes
|
|
||||||
let base = Registry::filter_label_chars(&preferred);
|
|
||||||
let base_len = base.len();
|
|
||||||
let mut candidate = base;
|
|
||||||
let mut suffix = CompactString::with_capacity(0);
|
|
||||||
loop {
|
loop {
|
||||||
candidate.push_str(&suffix);
|
candidate.0.push_str(&suffix);
|
||||||
let key = candidate.to_ascii_lowercase();
|
if !self.names.contains(&candidate) {
|
||||||
if !self.existing.contains(&key) {
|
self.key_to_suffix.insert(key_lower, Suffix(suffix));
|
||||||
self.label_transforms
|
self.names.insert(candidate.clone());
|
||||||
.insert(id, Transform::FilteredWithSuffix(suffix));
|
|
||||||
self.existing.insert(key);
|
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
candidate.truncate(base_len);
|
candidate.0.truncate(original_len);
|
||||||
increment(&mut suffix);
|
increment(&mut suffix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
candidate
|
candidate.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gen_anonymous(&mut self) -> CompactString {
|
pub fn gen_anonymous(&mut self) -> CompactString {
|
||||||
while self.existing.contains(&self.anonymous_counter) {
|
increment(&mut self.anonymous_counter);
|
||||||
|
while self
|
||||||
|
.names
|
||||||
|
.contains(&Lowercase(self.anonymous_counter.clone()))
|
||||||
|
{
|
||||||
increment(&mut self.anonymous_counter);
|
increment(&mut self.anonymous_counter);
|
||||||
}
|
}
|
||||||
let result = self.anonymous_counter.clone();
|
self.anonymous_counter.clone()
|
||||||
increment(&mut self.anonymous_counter);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid_label(s: &CompactString) -> bool {
|
|
||||||
if s.len() == 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let (most, last) = s.split_at(s.len() - 1);
|
|
||||||
if !most.chars().all(|c| c == '_' || c.is_ascii_alphabetic()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
last.chars().all(|c| c == '_' || c.is_ascii_alphanumeric())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn filter_label_chars(s: &CompactString) -> CompactString {
|
|
||||||
let mut result = CompactString::with_capacity(0);
|
|
||||||
let mut run_of_digits = false;
|
|
||||||
for c in s.chars() {
|
|
||||||
if c.is_ascii_digit() {
|
|
||||||
if !run_of_digits {
|
|
||||||
result.push('_');
|
|
||||||
run_of_digits = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.push(c);
|
|
||||||
run_of_digits = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given an unsanitized full name, generate the first-pick name we'd like to
|
/// Given the key string identifying a label, generate the first-pick name we'd
|
||||||
/// assign this label. (Results returned by this function are subject to veto
|
/// like to assign it. (Results returned by this function are subject to veto if
|
||||||
/// if they are invalid or already taken.)
|
/// they are already taken.)
|
||||||
fn preferred_label_name(s: &str) -> CompactString {
|
fn preferred_label_name(key: &str) -> CompactString {
|
||||||
if let Some((_, suffix)) = s.rsplit_once(|c: char| !c.is_ascii_alphanumeric() && c != '_') {
|
// Extract the last part of the key: "ns~global$123.local" becomes "local"
|
||||||
// Prevent namespaces and local labels from starting with a digit.
|
let base_name = key
|
||||||
// This ensures stuff like `#take gems 100.99orless` won't compile
|
.rsplit_once(|c: char| !c.is_ascii_alphanumeric() && c != '_')
|
||||||
// to `#take gems 10099orless` (would parse incorrectly).
|
.map(|(_, base_name)| base_name)
|
||||||
let mut result = CompactString::const_new("_");
|
.unwrap_or(key);
|
||||||
result.push_str(suffix);
|
|
||||||
result
|
// Collapse runs of numeric digits/underscores to a single underscore
|
||||||
} else {
|
let mut result = CompactString::default();
|
||||||
CompactString::new(s)
|
let mut run = false;
|
||||||
|
for c in base_name.chars() {
|
||||||
|
if c.is_ascii_alphabetic() {
|
||||||
|
result.push(c);
|
||||||
|
run = false;
|
||||||
|
} else if !run {
|
||||||
|
result.push('_');
|
||||||
|
run = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Increments a string through label-safe characters.
|
/// Increments a string through label-safe characters.
|
||||||
/// Characters loop through underscore and the letters a-z.
|
///
|
||||||
/// Additionally, the final character is allowed to loop through 0-9.
|
/// The characters tick upward in order (underscore, a-z), odometer style,
|
||||||
|
/// growing the string on overflow. The result is similar to counting in
|
||||||
|
/// base 27, but it's not quite the same:
|
||||||
|
///
|
||||||
|
/// - The zeroth value is the empty string "" (not "_").
|
||||||
|
/// - "z" is followed by "__", "_a", "_b", etc (not "a_", "aa", "ab").
|
||||||
|
///
|
||||||
|
/// Unlike in place-value systems where "0001" and "1" are equivalent, the goal
|
||||||
|
/// of this function is to generate every possible string.
|
||||||
fn increment(s: &mut CompactString) {
|
fn increment(s: &mut CompactString) {
|
||||||
let original_len = s.len();
|
let original_len = s.len();
|
||||||
|
|
||||||
// Find a character we can increment without carry
|
// Find a character we can increment without carry
|
||||||
let mut last_char = s.pop();
|
let mut last_char = s.pop();
|
||||||
while !(last_char == None || last_char != Some('z')) {
|
while last_char == Some('z') {
|
||||||
last_char = s.pop();
|
last_char = s.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(c) = last_char {
|
if let Some(c) = last_char {
|
||||||
// Increment character in order: 0-9, then _, then a-z
|
// Increment character
|
||||||
let incremented = match c {
|
let incremented = match c {
|
||||||
'0'..'9' | 'a'..'z' => (c as u8 + 1) as char,
|
'a'..'z' => (c as u8 + 1) as char,
|
||||||
'9' => '_',
|
|
||||||
'_' => 'a',
|
'_' => 'a',
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
s.push(incremented);
|
s.push(incremented);
|
||||||
|
|
||||||
// Pad to original length, like "___0"
|
// Pad to original length
|
||||||
while s.len() < original_len {
|
while s.len() < original_len {
|
||||||
let is_last = s.len() == original_len - 1;
|
|
||||||
s.push(if is_last { '0' } else { '_' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We reached maximum value for the string: everything rolls over
|
|
||||||
for _ in 0..original_len {
|
|
||||||
s.push('_');
|
s.push('_');
|
||||||
}
|
}
|
||||||
s.push('0');
|
} else {
|
||||||
|
// All existing chars roll over, length grows by 1
|
||||||
|
for _ in 0..(original_len + 1) {
|
||||||
|
s.push('_');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,29 +174,26 @@ mod test {
|
|||||||
}
|
}
|
||||||
let result = rows.join("\n");
|
let result = rows.join("\n");
|
||||||
assert_snapshot!(result, @r"
|
assert_snapshot!(result, @r"
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
|
|
||||||
_, a, b, c, d, e, f, g, h, i
|
_, a, b, c, d, e, f, g, h, i
|
||||||
j, k, l, m, n, o, p, q, r, s
|
j, k, l, m, n, o, p, q, r, s
|
||||||
t, u, v, w, x, y, z, _0, _1, _2
|
t, u, v, w, x, y, z, __, _a, _b
|
||||||
_3, _4, _5, _6, _7, _8, _9, __, _a, _b
|
|
||||||
_c, _d, _e, _f, _g, _h, _i, _j, _k, _l
|
_c, _d, _e, _f, _g, _h, _i, _j, _k, _l
|
||||||
_m, _n, _o, _p, _q, _r, _s, _t, _u, _v
|
_m, _n, _o, _p, _q, _r, _s, _t, _u, _v
|
||||||
_w, _x, _y, _z, a0, a1, a2, a3, a4, a5
|
_w, _x, _y, _z, a_, aa, ab, ac, ad, ae
|
||||||
a6, a7, a8, a9, a_, aa, ab, ac, ad, ae
|
|
||||||
af, ag, ah, ai, aj, ak, al, am, an, ao
|
af, ag, ah, ai, aj, ak, al, am, an, ao
|
||||||
|
ap, aq, ar, as, at, au, av, aw, ax, ay
|
||||||
|
az, b_, ba, bb, bc, bd, be, bf, bg, bh
|
||||||
|
bi, bj, bk, bl, bm, bn, bo, bp, bq, br
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_increment_all_len_3() {
|
fn test_increment_all_len_3() {
|
||||||
// Most chars are alphabetic or underscore (26 + 1 = 27).
|
let num_labels = 27 + 27 * 27 + 27 * 27 * 27; // 1 char + 2 chars + 3 chars
|
||||||
// Last char can include digits (27 + 10 = 37).
|
|
||||||
let num_labels = 37 + 27 * 37 + 27 * 27 * 37; // 1 char + 2 chars + 3 chars
|
|
||||||
let mut ctr = CompactString::new("");
|
let mut ctr = CompactString::new("");
|
||||||
let mut seen = HashSet::with_capacity(num_labels);
|
let mut seen = HashSet::with_capacity(num_labels);
|
||||||
for _ in 0..num_labels {
|
for _ in 0..num_labels {
|
||||||
increment(&mut ctr);
|
increment(&mut ctr);
|
||||||
assert!(Registry::is_valid_label(&ctr));
|
|
||||||
assert!(!seen.contains(&ctr));
|
assert!(!seen.contains(&ctr));
|
||||||
assert!(ctr.len() <= 3);
|
assert!(ctr.len() <= 3);
|
||||||
seen.insert(ctr.clone());
|
seen.insert(ctr.clone());
|
||||||
@@ -273,20 +234,20 @@ mod test {
|
|||||||
let result = results.join("\n");
|
let result = results.join("\n");
|
||||||
assert_snapshot!(result, @r#"
|
assert_snapshot!(result, @r#"
|
||||||
"foo" => foo
|
"foo" => foo
|
||||||
"ns1~foo" => _foo
|
"ns1~foo" => foo_
|
||||||
"ns2~foo" => _foo0
|
"ns2~foo" => fooa
|
||||||
"foo.thisloop" => _thisloop
|
"foo.thisloop" => thisloop
|
||||||
"foo.thatloop" => _thatloop
|
"foo.thatloop" => thatloop
|
||||||
"bar.thisloop" => _thisloop0
|
"bar.thisloop" => thisloop_
|
||||||
"bar.thatloop" => _thatloop0
|
"bar.thatloop" => thatloop_
|
||||||
"foo$1.thisloop" => _thisloop1
|
"foo$1.thisloop" => thisloopa
|
||||||
"foo$1.thatloop" => _thatloop1
|
"foo$1.thatloop" => thatloopa
|
||||||
"bar1" => bar1
|
"bar1" => bar_
|
||||||
"bar2" => bar2
|
"bar2" => bar__
|
||||||
"bar123" => bar_
|
"bar123" => bar_a
|
||||||
"BAR123" => BAR_
|
"BAR123" => BAR_a
|
||||||
"bar456" => bar_0
|
"bar456" => bar_b
|
||||||
"BAR456" => BAR_0
|
"BAR456" => BAR_b
|
||||||
"foo2bar" => foo_bar
|
"foo2bar" => foo_bar
|
||||||
"#);
|
"#);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ expression: board_to_text(board)
|
|||||||
You have hundreds of gems.
|
You have hundreds of gems.
|
||||||
#end
|
#end
|
||||||
:lt_
|
:lt_
|
||||||
#take gems 10 lt_0
|
#take gems 10 lt__
|
||||||
#give gems 10
|
#give gems 10
|
||||||
You have tens of gems.
|
You have tens of gems.
|
||||||
#end
|
#end
|
||||||
:lt_0
|
:lt__
|
||||||
#take gems 1 lt1
|
#take gems 1 lt_a
|
||||||
#give gems 1
|
#give gems 1
|
||||||
You have at least one gem.
|
You have at least one gem.
|
||||||
#end
|
#end
|
||||||
:lt1
|
:lt_a
|
||||||
You don't have any gems!
|
You don't have any gems!
|
||||||
|
|||||||
@@ -6,50 +6,77 @@ expression: board_to_text(board)
|
|||||||
'using the same local label ".loop"
|
'using the same local label ".loop"
|
||||||
:run_w
|
:run_w
|
||||||
#walk w
|
#walk w
|
||||||
:_loop
|
:loop
|
||||||
#try w stop
|
#try w stop
|
||||||
#_loop
|
#loop
|
||||||
'
|
'
|
||||||
:run_e
|
:run_e
|
||||||
#walk e
|
#walk e
|
||||||
:_loop0
|
:loop_
|
||||||
#try e stop
|
#try e stop
|
||||||
#_loop0
|
#loop_
|
||||||
'
|
'
|
||||||
:stop
|
:stop
|
||||||
#walk i
|
#walk i
|
||||||
---
|
---
|
||||||
'Using locals before any globals are defined
|
'Using locals before any globals are defined
|
||||||
:_loop1
|
:loopa
|
||||||
#take gems 1 _break
|
#take gems 1 break
|
||||||
#_loop1
|
#loopa
|
||||||
:_break
|
:break
|
||||||
---
|
---
|
||||||
'Multiple sections with the same name
|
'Multiple sections with the same name
|
||||||
#end
|
#end
|
||||||
:touch
|
:touch
|
||||||
Trying to sell you some ammo...
|
Trying to sell you some ammo...
|
||||||
#take gems 10 _skip
|
#take gems 10 skip
|
||||||
#give ammo 10
|
#give ammo 10
|
||||||
:_skip
|
:skip
|
||||||
#zap touch
|
#zap touch
|
||||||
#end
|
#end
|
||||||
:touch
|
:touch
|
||||||
Trying to sell you some torches...
|
Trying to sell you some torches...
|
||||||
#take gems 15 _skip0
|
#take gems 15 skip_
|
||||||
#give torches 5
|
#give torches 5
|
||||||
:_skip0
|
:skip_
|
||||||
#restore touch
|
#restore touch
|
||||||
---
|
---
|
||||||
'Multiple locals with the same name
|
'Multiple locals with the same name
|
||||||
#end
|
#end
|
||||||
:repeat3
|
:repeat_
|
||||||
:_z
|
:z
|
||||||
:_z
|
:z
|
||||||
:_z
|
:z
|
||||||
#take gems 1 _skip1
|
#take gems 1 skipa
|
||||||
#give score 1
|
#give score 1
|
||||||
:_skip1
|
:skipa
|
||||||
#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:
|
||||||
|
#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
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ expression: board_to_text(board)
|
|||||||
'Make sure locals exist in separate namespaces
|
'Make sure locals exist in separate namespaces
|
||||||
:do_stuff
|
:do_stuff
|
||||||
#lock
|
#lock
|
||||||
:_loop
|
:loop
|
||||||
'
|
'
|
||||||
:_do_stuff
|
:do_stuff_
|
||||||
:_loop0
|
:loop_
|
||||||
/i#if blocked rndne _loop0
|
/i#if blocked rndne loop_
|
||||||
'
|
'
|
||||||
#take time 1 _break
|
#take time 1 break
|
||||||
#_loop
|
#loop
|
||||||
:_break
|
:break
|
||||||
#unlock
|
#unlock
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+1
-1
@@ -207,7 +207,7 @@ impl<'a, T: Clone> Capture<'a, T> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use insta::assert_debug_snapshot;
|
use insta::assert_debug_snapshot;
|
||||||
use peg_macro::grammar;
|
use mzp_peg_macro::grammar;
|
||||||
|
|
||||||
use super::ParseState;
|
use super::ParseState;
|
||||||
|
|
||||||
|
|||||||
+19
-21
@@ -7,7 +7,6 @@ use nom::{
|
|||||||
error::{ErrorKind, ParseError},
|
error::{ErrorKind, ParseError},
|
||||||
multi::count,
|
multi::count,
|
||||||
number::complete::{le_i16, le_u8, le_u16},
|
number::complete::{le_i16, le_u8, le_u16},
|
||||||
sequence::tuple,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
use crate::encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
||||||
@@ -128,14 +127,13 @@ pub struct Stat {
|
|||||||
|
|
||||||
impl World {
|
impl World {
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Result<World, LoadError> {
|
pub fn from_bytes(bytes: &[u8]) -> Result<World, LoadError> {
|
||||||
let (input, (_, num_boards)) = tuple((tag([0xff, 0xff]), le_i16)).parse(bytes)?;
|
let (input, (_, num_boards)) = (tag(&[0xff, 0xff][..]), le_i16).parse(bytes)?;
|
||||||
let (input, (ammo, gems, keys)) =
|
let (input, (ammo, gems, keys)) = (le_i16, le_i16, count(bool_u8, 7)).parse(input)?;
|
||||||
tuple((le_i16, le_i16, count(bool_u8, 7))).parse(input)?;
|
|
||||||
let (input, (health, starting_board, torches, torch_cycles, energizer_cycles)) =
|
let (input, (health, starting_board, torches, torch_cycles, energizer_cycles)) =
|
||||||
tuple((le_i16, le_i16, le_i16, le_i16, le_i16)).parse(input)?;
|
(le_i16, le_i16, le_i16, le_i16, le_i16).parse(input)?;
|
||||||
let (input, (_, score, world_name)) = tuple((take(2usize), le_i16, pstring(20)))(input)?;
|
let (input, (_, score, world_name)) = (take(2usize), le_i16, pstring(20)).parse(input)?;
|
||||||
let (input, flags) = count(pstring(20), 10).parse(input)?;
|
let (input, flags) = count(pstring(20), 10).parse(input)?;
|
||||||
let (_input, (time, time_ticks, locked)) = tuple((le_i16, le_i16, bool_u8)).parse(input)?;
|
let (_input, (time, time_ticks, locked)) = (le_i16, le_i16, bool_u8).parse(input)?;
|
||||||
|
|
||||||
// Rest of header is padding; fast-forward starting from original input
|
// Rest of header is padding; fast-forward starting from original input
|
||||||
let (input, _) = take(512usize).parse(bytes)?;
|
let (input, _) = take(512usize).parse(bytes)?;
|
||||||
@@ -214,7 +212,7 @@ impl Board {
|
|||||||
let mut input = input;
|
let mut input = input;
|
||||||
let mut terrain = Vec::with_capacity(NUM_TILES);
|
let mut terrain = Vec::with_capacity(NUM_TILES);
|
||||||
while terrain.len() < NUM_TILES {
|
while terrain.len() < NUM_TILES {
|
||||||
let (next_input, (count, element, color)) = tuple((le_u8, le_u8, le_u8))(input)?;
|
let (next_input, (count, element, color)) = (le_u8, le_u8, le_u8).parse(input)?;
|
||||||
input = next_input;
|
input = next_input;
|
||||||
let count: u32 = if count == 0 { 256 } else { count.into() };
|
let count: u32 = if count == 0 { 256 } else { count.into() };
|
||||||
for _ in 0..count {
|
for _ in 0..count {
|
||||||
@@ -226,11 +224,11 @@ impl Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read board info
|
// Read board info
|
||||||
let (input, (max_shots, is_dark)) = tuple((le_u8, bool_u8))(input)?;
|
let (input, (max_shots, is_dark)) = (le_u8, bool_u8).parse(input)?;
|
||||||
let (input, (board_n, board_s, board_w, board_e)) =
|
let (input, (board_n, board_s, board_w, board_e)) =
|
||||||
tuple((le_u8, le_u8, le_u8, le_u8))(input)?;
|
(le_u8, le_u8, le_u8, le_u8).parse(input)?;
|
||||||
let (input, (reenter_when_zapped, message)) = tuple((bool_u8, pstring(58)))(input)?;
|
let (input, (reenter_when_zapped, message)) = (bool_u8, pstring(58)).parse(input)?;
|
||||||
let (input, (enter_x, enter_y, time_limit)) = tuple((le_u8, le_u8, le_i16))(input)?;
|
let (input, (enter_x, enter_y, time_limit)) = (le_u8, le_u8, le_i16).parse(input)?;
|
||||||
let (input, _) = take(16usize)(input)?;
|
let (input, _) = take(16usize)(input)?;
|
||||||
|
|
||||||
// Read stats
|
// Read stats
|
||||||
@@ -239,7 +237,7 @@ impl Board {
|
|||||||
if num_stats < 0 {
|
if num_stats < 0 {
|
||||||
return Err("cannot have a negative number of stats".into());
|
return Err("cannot have a negative number of stats".into());
|
||||||
}
|
}
|
||||||
let (_input, stats) = count(Stat::from_bytes, num_stats as usize)(input)?;
|
let (_input, stats) = count(Stat::from_bytes, num_stats as usize).parse(input)?;
|
||||||
|
|
||||||
Ok(Board {
|
Ok(Board {
|
||||||
name,
|
name,
|
||||||
@@ -315,12 +313,12 @@ impl Board {
|
|||||||
|
|
||||||
impl Stat {
|
impl Stat {
|
||||||
pub fn from_bytes(input: &[u8]) -> IResult<&[u8], Self, LoadError> {
|
pub fn from_bytes(input: &[u8]) -> IResult<&[u8], Self, LoadError> {
|
||||||
let (input, (x, y, x_step, y_step)) = tuple((le_u8, le_u8, le_i16, le_i16))(input)?;
|
let (input, (x, y, x_step, y_step)) = (le_u8, le_u8, le_i16, le_i16).parse(input)?;
|
||||||
let (input, (cycle, p1, p2, p3)) = tuple((le_i16, le_u8, le_u8, le_u8))(input)?;
|
let (input, (cycle, p1, p2, p3)) = (le_i16, le_u8, le_u8, le_u8).parse(input)?;
|
||||||
let (input, (follower, leader)) = tuple((le_i16, le_i16))(input)?;
|
let (input, (follower, leader)) = (le_i16, le_i16).parse(input)?;
|
||||||
let (input, (under_element, under_color)) = tuple((le_u8, le_u8))(input)?;
|
let (input, (under_element, under_color)) = (le_u8, le_u8).parse(input)?;
|
||||||
let (input, _) = take(4usize)(input)?;
|
let (input, _) = take(4usize)(input)?;
|
||||||
let (input, (instruction_pointer, length)) = tuple((le_i16, le_i16))(input)?;
|
let (input, (instruction_pointer, length)) = (le_i16, le_i16).parse(input)?;
|
||||||
let (input, _) = take(8usize)(input)?;
|
let (input, _) = take(8usize)(input)?;
|
||||||
let (input, code_bytes) = take(0.max(length) as usize)(input)?;
|
let (input, code_bytes) = take(0.max(length) as usize)(input)?;
|
||||||
let code = decode_multiline(&code_bytes);
|
let code = decode_multiline(&code_bytes);
|
||||||
@@ -363,15 +361,15 @@ impl Stat {
|
|||||||
result.push_padding(4);
|
result.push_padding(4);
|
||||||
result.push_i16(self.instruction_pointer);
|
result.push_i16(self.instruction_pointer);
|
||||||
// TODO: more safety around valid bind-indexes (positive? negative?)
|
// TODO: more safety around valid bind-indexes (positive? negative?)
|
||||||
|
let code_bytes = encode_multiline(&self.code).unwrap();
|
||||||
result.push_i16(if self.bind_index < 0 {
|
result.push_i16(if self.bind_index < 0 {
|
||||||
self.bind_index
|
self.bind_index
|
||||||
} else {
|
} else {
|
||||||
self.code.len() as i16
|
code_bytes.len() as i16
|
||||||
});
|
});
|
||||||
result.push_padding(8);
|
result.push_padding(8);
|
||||||
if self.bind_index >= 0 {
|
if self.bind_index >= 0 {
|
||||||
// TODO: more safety around bind-index XOR code
|
// TODO: more safety around bind-index XOR code
|
||||||
let code_bytes = encode_multiline(&self.code).unwrap();
|
|
||||||
result.extend_from_slice(&code_bytes);
|
result.extend_from_slice(&code_bytes);
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
@@ -387,7 +385,7 @@ fn pstring(cap: u8) -> impl Fn(&[u8]) -> IResult<&[u8], Vec<u8>, LoadError> {
|
|||||||
move |input: &[u8]| -> IResult<&[u8], Vec<u8>, LoadError> {
|
move |input: &[u8]| -> IResult<&[u8], Vec<u8>, LoadError> {
|
||||||
let (input, len) = le_u8(input)?;
|
let (input, len) = le_u8(input)?;
|
||||||
if len >= cap {
|
if len >= cap {
|
||||||
return fail(input);
|
return fail().parse(input);
|
||||||
}
|
}
|
||||||
let (input, data) = take(len)(input)?;
|
let (input, data) = take(len)(input)?;
|
||||||
let (input, _) = take(cap - len)(input)?;
|
let (input, _) = take(cap - len)(input)?;
|
||||||
|
|||||||
@@ -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