Compare commits
10 Commits
3afc112810
...
452fad3302
| Author | SHA1 | Date | |
|---|---|---|---|
| 452fad3302 | |||
| d699340ec8 | |||
| e8fa45fc3b | |||
| 84f35c8b44 | |||
| 85ba7b63c8 | |||
| f6df3222ab | |||
| 8817cd8e2e | |||
| 3bddc3c85e | |||
| d751aade99 | |||
| 051e08fba9 |
Generated
+147
-22
@@ -10,9 +10,9 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
@@ -91,6 +91,28 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.43.1"
|
||||
@@ -134,13 +156,12 @@ name = "marzipan"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codepage-437",
|
||||
"compact_str",
|
||||
"insta",
|
||||
"lexopt",
|
||||
"nom",
|
||||
"peg_macro",
|
||||
"mzp-peg-macro",
|
||||
"rustc-hash",
|
||||
"zztff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -150,19 +171,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
name = "mzp-peg-macro"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -172,13 +218,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "peg_macro"
|
||||
version = "0.1.0"
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -219,18 +264,27 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -260,6 +314,56 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.11+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
@@ -338,3 +442,24 @@ name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zztff"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f81bebb421e202e22f22f88427f98582ab74dc846a05ffc6da1527cbe6cdae9d"
|
||||
dependencies = [
|
||||
"codepage-437",
|
||||
"nom",
|
||||
"num_enum",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
+8
-9
@@ -1,16 +1,19 @@
|
||||
[package]
|
||||
name = "marzipan"
|
||||
description = "A macro processor for ZZT worlds"
|
||||
keywords = ["zzt", "gamedev"]
|
||||
categories = ["compilers", "command-line-utilities"]
|
||||
version = "0.1.0"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
codepage-437 = "0.1.0"
|
||||
peg_macro = { path = "./peg_macro"}
|
||||
insta = "1.39.0"
|
||||
nom = "7.1.3"
|
||||
mzp-peg-macro = { path = "./mzp_peg_macro", version = "0.1.0" }
|
||||
zztff = "0.3"
|
||||
compact_str = "0.9.0"
|
||||
rustc-hash = "2.1.1"
|
||||
lexopt = "0.3.1"
|
||||
@@ -22,9 +25,5 @@ insta = "1.39.0"
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[[bin]]
|
||||
name = "marzipan"
|
||||
path = "src/main.rs"
|
||||
|
||||
[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]
|
||||
name = "peg_macro"
|
||||
name = "mzp-peg-macro"
|
||||
description = "Internal crate for generating Marzipan's PEG parser code"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/cmounce/marzipan"
|
||||
|
||||
[lib]
|
||||
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.
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use codepage_437::CP437_WINGDINGS;
|
||||
|
||||
// Serialize multi-line content with CR-terminated lines
|
||||
pub fn encode_multiline(input: &str) -> Result<Vec<u8>> {
|
||||
input
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c == '\n' {
|
||||
Ok('\r' as u8)
|
||||
} else {
|
||||
CP437_WINGDINGS
|
||||
.encode(c)
|
||||
.ok_or(anyhow!("Couldn't encode char: {}", c))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn decode_multiline(input: &[u8]) -> String {
|
||||
input
|
||||
.iter()
|
||||
.map(|&x| {
|
||||
if x == '\r' as u8 {
|
||||
'\n'
|
||||
} else {
|
||||
CP437_WINGDINGS.decode(x)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn encode_oneline(input: &str) -> Result<Vec<u8>> {
|
||||
input
|
||||
.chars()
|
||||
.map(|c| {
|
||||
CP437_WINGDINGS
|
||||
.encode(c)
|
||||
.ok_or(anyhow!("Couldn't encode char: {}", c))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn decode_oneline(input: &[u8]) -> String {
|
||||
input.iter().map(|&x| CP437_WINGDINGS.decode(x)).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use insta::assert_debug_snapshot;
|
||||
|
||||
fn serialize_code(input: &str) -> Vec<u8> {
|
||||
encode_multiline(input).expect("Error in test")
|
||||
}
|
||||
|
||||
fn serialize_title(input: &str) -> Vec<u8> {
|
||||
encode_oneline(input).expect("Error in test")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_multiline() {
|
||||
let bytes: Vec<u8> = (0..=255).collect();
|
||||
assert_eq!(bytes, serialize_code(&decode_multiline(&bytes)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_oneline() {
|
||||
let bytes: Vec<u8> = (0..=255).collect();
|
||||
assert_eq!(bytes, serialize_title(&decode_oneline(&bytes)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newlines_to_cr() {
|
||||
assert_debug_snapshot!(serialize_code("ABC\nDEF"), @r###"
|
||||
[
|
||||
65,
|
||||
66,
|
||||
67,
|
||||
13,
|
||||
68,
|
||||
69,
|
||||
70,
|
||||
]
|
||||
"###)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_13_to_wingding() {
|
||||
// Code is allowed to have newlines
|
||||
let bytes = serialize_code("ABC\nDEF");
|
||||
assert!(bytes.contains(&13));
|
||||
|
||||
// But in a board title, that same byte is a wingding
|
||||
assert_debug_snapshot!(decode_oneline(&bytes), @r#""ABC♪DEF""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wingdings() {
|
||||
assert_debug_snapshot!(decode_multiline(&(0..32).collect::<Vec<_>>()), @r###""\0☺☻♥♦♣♠•◘○◙♂♀\n♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼""###);
|
||||
assert_debug_snapshot!(decode_multiline(&(112..144).collect::<Vec<_>>()), @r###""pqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅ""###);
|
||||
assert_debug_snapshot!(decode_multiline(&(224..=255).collect::<Vec<_>>()), @r###""αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}""###);
|
||||
}
|
||||
}
|
||||
+17
-6
@@ -1,6 +1,6 @@
|
||||
use std::{cell::RefCell, collections::VecDeque, error::Error, fmt::Display, ops::Range};
|
||||
|
||||
use crate::world::World;
|
||||
use zztff::{Program, World};
|
||||
|
||||
pub enum Context<'a> {
|
||||
Base(Box<RefCell<Vec<CompileMessage>>>),
|
||||
@@ -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> {
|
||||
match self {
|
||||
Context::Base(refcell) => {
|
||||
@@ -102,7 +109,7 @@ pub struct CompileMessage {
|
||||
pub location: Location,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Level {
|
||||
Error,
|
||||
Warning,
|
||||
@@ -141,20 +148,24 @@ impl CompileMessage {
|
||||
}
|
||||
let board = location.board.map(|i| &world.boards[i]);
|
||||
if let Some(board) = board {
|
||||
breadcrumbs.push(board.name.clone());
|
||||
breadcrumbs.push(board.title.clone());
|
||||
}
|
||||
let stat = board.and_then(|board| location.stat.map(|i| &board.stats[i]));
|
||||
let code = stat.and_then(|stat| match &stat.program {
|
||||
Program::Own(code) => Some(code.as_str()),
|
||||
Program::Bound(_) => None,
|
||||
});
|
||||
if let Some(stat) = stat {
|
||||
let first_line = stat.code.lines().next();
|
||||
let first_line = code.and_then(|x| x.lines().next());
|
||||
let name = first_line.filter(|x| x.starts_with("@")).unwrap_or("stat");
|
||||
let (x, y) = (stat.x, stat.y);
|
||||
breadcrumbs.push(format!("{name} ({x},{y})"));
|
||||
}
|
||||
let span = stat.and_then(|stat| {
|
||||
let span = code.and_then(|code| {
|
||||
location
|
||||
.span
|
||||
.as_ref()
|
||||
.map(|span| RichSpan::new(&span, &stat.code))
|
||||
.map(|span| RichSpan::new(&span, code))
|
||||
});
|
||||
if let Some(ref span) = span {
|
||||
let line = span.line_number;
|
||||
|
||||
+7
-3
@@ -3,7 +3,8 @@ use std::ops::Range;
|
||||
use compact_str::CompactString;
|
||||
use grammar::Tag;
|
||||
|
||||
use crate::{error::Context, peg::ParseState, world::Stat};
|
||||
use crate::{error::Context, peg::ParseState};
|
||||
use zztff::{Program, Stat};
|
||||
|
||||
pub type ParsedStat = Vec<Chunk>;
|
||||
|
||||
@@ -26,7 +27,10 @@ pub struct LabelName {
|
||||
}
|
||||
|
||||
pub fn parse_stat_labels(stat: &Stat, ctx: &Context) -> ParsedStat {
|
||||
let code = &stat.code;
|
||||
let code = match &stat.program {
|
||||
Program::Own(code) => code,
|
||||
Program::Bound(_) => return Vec::new(),
|
||||
};
|
||||
let mut parser = ParseState::new(code);
|
||||
assert!(
|
||||
grammar::program(&mut parser),
|
||||
@@ -121,7 +125,7 @@ pub fn parse_stat_labels(stat: &Stat, ctx: &Context) -> ParsedStat {
|
||||
}
|
||||
|
||||
mod grammar {
|
||||
use peg_macro::grammar;
|
||||
use mzp_peg_macro::grammar;
|
||||
|
||||
grammar! {
|
||||
program = (line ("\n" line)*)? EOI;
|
||||
|
||||
+110
-64
@@ -1,14 +1,15 @@
|
||||
use compact_str::CompactString;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{error::Context, world::Board};
|
||||
use crate::error::Context;
|
||||
use zztff::{Board, Program};
|
||||
|
||||
use super::{
|
||||
parse::{Chunk, ParsedStat, parse_stat_labels},
|
||||
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();
|
||||
|
||||
// Parse stats into chunks
|
||||
@@ -20,16 +21,30 @@ pub fn process_labels(board: &Board, ctx: &Context) -> Board {
|
||||
.collect();
|
||||
let mut registry = Registry::new();
|
||||
|
||||
// Expand ".local" labels to full "section.local" form.
|
||||
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_backward_pass(&mut stats, ctx);
|
||||
|
||||
// Join chunks together and replace old stats' code
|
||||
for (old_stat, parsed_stat) in board.stats.iter_mut().zip(stats.into_iter()) {
|
||||
let new_code = parsed_stat
|
||||
// Only update the stat's code string if it had a code string to begin with.
|
||||
//
|
||||
// This ensures we don't accidentally un-bind a stat by overwriting its binding with an
|
||||
// empty code string. It would probably be cleaner if we refactored the code to use
|
||||
// Option<ParsedStat> or similar, so that bound stats would never lead to an empty string
|
||||
// existing in the first place.
|
||||
if let Program::Own(code) = &mut old_stat.program {
|
||||
*code = parsed_stat
|
||||
.into_iter()
|
||||
.map(|chunk| match chunk {
|
||||
Chunk::Verbatim(s) => s,
|
||||
@@ -40,10 +55,10 @@ pub fn process_labels(board: &Board, ctx: &Context) -> Board {
|
||||
} => name.name.into(),
|
||||
})
|
||||
.collect();
|
||||
old_stat.code = new_code;
|
||||
}
|
||||
}
|
||||
|
||||
board
|
||||
(!ctx.any_errors()).then_some(board)
|
||||
}
|
||||
|
||||
/// Resolve ".local" labels to "name.local" form.
|
||||
@@ -51,15 +66,8 @@ fn resolve_local_labels(stats: &mut [ParsedStat], ctx: &Context) {
|
||||
for (i, stat) in stats.iter_mut().enumerate() {
|
||||
let ctx = ctx.with_stat(i);
|
||||
|
||||
// Helper: Generate unique section strings like "touch$0"
|
||||
let mut i = 0;
|
||||
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> =
|
||||
// Keep a separate resolver for each namespace
|
||||
let mut resolvers: FxHashMap<Option<CompactString>, LocalLabelResolver> =
|
||||
FxHashMap::default();
|
||||
|
||||
for chunk in stat.iter_mut() {
|
||||
@@ -69,33 +77,22 @@ fn resolve_local_labels(stats: &mut [ParsedStat], ctx: &Context) {
|
||||
is_anon: false,
|
||||
name: label,
|
||||
} => {
|
||||
// Handle cases where either one of (section name, label) is missing.
|
||||
// If both are present ("name.local") then the label is already fully resolved
|
||||
// and there is nothing to do.
|
||||
let resolver = resolvers.entry(label.namespace.clone()).or_default();
|
||||
let is_definition = !*is_ref;
|
||||
if let Some(local) = &label.local {
|
||||
if label.name.is_empty() {
|
||||
// Expand :.local to :name.local
|
||||
assert!(label.local.is_some());
|
||||
label.name = if let Some(section) =
|
||||
namespace_to_section.get(&label.namespace)
|
||||
{
|
||||
section.clone()
|
||||
} 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.
|
||||
// Local that needs to be resolved, such as ":.foo" or "#send .foo"
|
||||
label.name = resolver.get_section_prefix(&local)
|
||||
} else if is_definition {
|
||||
// Illegal local label definition, such as ":touch.foo"
|
||||
// _References_ to local labels may specify a section name: "#send touch.foo".
|
||||
// But when a local is _defined_, the section name must always be inferred.
|
||||
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 +101,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.
|
||||
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 chunk in stat.iter_mut() {
|
||||
match chunk {
|
||||
@@ -140,6 +188,8 @@ fn anonymous_forward_pass(stats: &mut [ParsedStat], registry: &mut Registry, ctx
|
||||
let mut label_names = vec![];
|
||||
|
||||
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
|
||||
let mut i = 0;
|
||||
let mut get_next_name = || -> CompactString {
|
||||
@@ -155,7 +205,6 @@ fn anonymous_forward_pass(stats: &mut [ParsedStat], registry: &mut Registry, ctx
|
||||
let mut namespace_to_latest: FxHashMap<Option<CompactString>, CompactString> =
|
||||
FxHashMap::default();
|
||||
|
||||
let ctx = ctx.with_stat(stat_index);
|
||||
for chunk in stat.iter_mut() {
|
||||
match chunk {
|
||||
Chunk::Label {
|
||||
@@ -190,8 +239,9 @@ fn anonymous_forward_pass(stats: &mut [ParsedStat], registry: &mut Registry, ctx
|
||||
/// Resolve anonymous forward references to their label names.
|
||||
fn anonymous_backward_pass(stats: &mut [ParsedStat], ctx: &Context) {
|
||||
for (stat_index, stat) in stats.iter_mut().enumerate() {
|
||||
let mut namespace_to_latest = FxHashMap::default();
|
||||
let ctx = ctx.with_stat(stat_index);
|
||||
|
||||
let mut namespace_to_latest = FxHashMap::default();
|
||||
for chunk in stat.iter_mut().rev() {
|
||||
match chunk {
|
||||
Chunk::Label {
|
||||
@@ -227,10 +277,8 @@ mod test {
|
||||
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::{
|
||||
error::Context,
|
||||
world::{Board, Stat, World},
|
||||
};
|
||||
use crate::error::Context;
|
||||
use zztff::{Board, Program, Stat, World};
|
||||
|
||||
use super::process_labels;
|
||||
|
||||
@@ -244,19 +292,10 @@ mod test {
|
||||
.map(|code| Stat {
|
||||
x: 1,
|
||||
y: 1,
|
||||
x_step: 0,
|
||||
y_step: 0,
|
||||
cycle: 3,
|
||||
p1: 2,
|
||||
p2: 0,
|
||||
p3: 0,
|
||||
follower: -1,
|
||||
leader: -1,
|
||||
under_element: 0,
|
||||
under_color: 0,
|
||||
instruction_pointer: 0,
|
||||
bind_index: 0,
|
||||
code,
|
||||
program: Program::Own(code),
|
||||
..Stat::default()
|
||||
})
|
||||
.collect();
|
||||
board
|
||||
@@ -269,35 +308,42 @@ mod test {
|
||||
}
|
||||
|
||||
fn board_to_text(board: Board) -> String {
|
||||
let codes: Vec<_> = board.stats.into_iter().map(|stat| stat.code).collect();
|
||||
let codes: Vec<_> = board
|
||||
.stats
|
||||
.into_iter()
|
||||
.filter_map(|stat| match stat.program {
|
||||
Program::Own(code) => Some(code),
|
||||
Program::Bound(_) => None,
|
||||
})
|
||||
.collect();
|
||||
codes.join("---\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_label_sanitization() {
|
||||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anonymous_labels() {
|
||||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_labels() {
|
||||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_namespaces() {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
+104
-143
@@ -1,187 +1,151 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use compact_str::CompactString;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
struct LabelId(CompactString);
|
||||
pub struct Registry {
|
||||
key_to_suffix: FxHashMap<Lowercase, Suffix>,
|
||||
names: FxHashSet<Lowercase>,
|
||||
anonymous_counter: CompactString,
|
||||
}
|
||||
|
||||
impl LabelId {
|
||||
fn new(label: &str) -> Self {
|
||||
let mut result = CompactString::new(label);
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
struct Lowercase(CompactString);
|
||||
|
||||
impl Lowercase {
|
||||
fn new(key: &str) -> Self {
|
||||
let mut result = CompactString::new(key);
|
||||
result.make_ascii_lowercase();
|
||||
Self(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Registry {
|
||||
label_transforms: HashMap<LabelId, Transform>,
|
||||
existing: HashSet<CompactString>,
|
||||
anonymous_counter: CompactString,
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct Suffix(CompactString);
|
||||
|
||||
enum Transform {
|
||||
Preferred,
|
||||
FilteredWithSuffix(CompactString),
|
||||
}
|
||||
|
||||
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);
|
||||
impl Suffix {
|
||||
fn apply(&self, key: &str) -> CompactString {
|
||||
let mut result = preferred_label_name(key);
|
||||
result.push_str(&self.0);
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn new() -> Self {
|
||||
let builtin_labels = ["bombed", "energize", "shot", "thud", "touch"];
|
||||
let mut label_transforms = HashMap::new();
|
||||
let mut existing = HashSet::new();
|
||||
let mut key_to_suffix = FxHashMap::default();
|
||||
let mut taken = FxHashSet::default();
|
||||
for name in builtin_labels {
|
||||
let cs = CompactString::new(name);
|
||||
label_transforms.insert(LabelId(cs.clone()), Transform::Preferred);
|
||||
existing.insert(cs);
|
||||
let name = Lowercase(name.into());
|
||||
key_to_suffix.insert(name.clone(), Suffix::default());
|
||||
taken.insert(name);
|
||||
}
|
||||
Self {
|
||||
label_transforms,
|
||||
existing,
|
||||
anonymous_counter: "_".into(),
|
||||
key_to_suffix,
|
||||
names: taken,
|
||||
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
|
||||
let id = LabelId::new(label);
|
||||
if let Some(transform) = self.label_transforms.get(&id) {
|
||||
return transform.apply(label);
|
||||
let key_lower = Lowercase::new(key);
|
||||
if let Some(transform) = self.key_to_suffix.get(&key_lower) {
|
||||
return transform.apply(key);
|
||||
}
|
||||
|
||||
// Try to use label's preferred name as-is
|
||||
let preferred = Transform::Preferred.apply(label);
|
||||
if Registry::is_valid_label(&preferred) {
|
||||
let key = preferred.to_ascii_lowercase();
|
||||
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);
|
||||
// Append suffixes until we find a name that's not taken yet
|
||||
let mut candidate = Lowercase::new(&preferred_label_name(key));
|
||||
let mut suffix = CompactString::const_new("");
|
||||
let original_len = candidate.0.len();
|
||||
loop {
|
||||
candidate.push_str(&suffix);
|
||||
let key = candidate.to_ascii_lowercase();
|
||||
if !self.existing.contains(&key) {
|
||||
self.label_transforms
|
||||
.insert(id, Transform::FilteredWithSuffix(suffix));
|
||||
self.existing.insert(key);
|
||||
candidate.0.push_str(&suffix);
|
||||
if !self.names.contains(&candidate) {
|
||||
self.key_to_suffix.insert(key_lower, Suffix(suffix));
|
||||
self.names.insert(candidate.clone());
|
||||
break;
|
||||
} else {
|
||||
candidate.truncate(base_len);
|
||||
candidate.0.truncate(original_len);
|
||||
increment(&mut suffix);
|
||||
}
|
||||
}
|
||||
candidate
|
||||
candidate.0
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
let result = self.anonymous_counter.clone();
|
||||
increment(&mut self.anonymous_counter);
|
||||
result
|
||||
self.anonymous_counter.clone()
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
/// Given the key string identifying a label, generate the first-pick name we'd
|
||||
/// like to assign it. (Results returned by this function are subject to veto if
|
||||
/// they are already taken.)
|
||||
fn preferred_label_name(key: &str) -> CompactString {
|
||||
// Extract the last part of the key: "ns~global$123.local" becomes "local"
|
||||
let base_name = key
|
||||
.rsplit_once(|c: char| !c.is_ascii_alphanumeric() && c != '_')
|
||||
.map(|(_, base_name)| base_name)
|
||||
.unwrap_or(key);
|
||||
|
||||
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 {
|
||||
// Collapse runs of numeric digits/underscores to a single underscore
|
||||
let mut result = CompactString::default();
|
||||
let mut run = false;
|
||||
for c in base_name.chars() {
|
||||
if c.is_ascii_alphabetic() {
|
||||
result.push(c);
|
||||
run_of_digits = false;
|
||||
run = false;
|
||||
} else if !run {
|
||||
result.push('_');
|
||||
run = true;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Given an unsanitized full name, generate the first-pick name we'd like to
|
||||
/// assign this label. (Results returned by this function are subject to veto
|
||||
/// if they are invalid or already taken.)
|
||||
fn preferred_label_name(s: &str) -> CompactString {
|
||||
if let Some((_, suffix)) = s.rsplit_once(|c: char| !c.is_ascii_alphanumeric() && c != '_') {
|
||||
// Prevent namespaces and local labels from starting with a digit.
|
||||
// This ensures stuff like `#take gems 100.99orless` won't compile
|
||||
// to `#take gems 10099orless` (would parse incorrectly).
|
||||
let mut result = CompactString::const_new("_");
|
||||
result.push_str(suffix);
|
||||
result
|
||||
} else {
|
||||
CompactString::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
let original_len = s.len();
|
||||
|
||||
// Find a character we can increment without carry
|
||||
let mut last_char = s.pop();
|
||||
while !(last_char == None || last_char != Some('z')) {
|
||||
while last_char == Some('z') {
|
||||
last_char = s.pop();
|
||||
}
|
||||
|
||||
if let Some(c) = last_char {
|
||||
// Increment character in order: 0-9, then _, then a-z
|
||||
// Increment character
|
||||
let incremented = match c {
|
||||
'0'..'9' | 'a'..'z' => (c as u8 + 1) as char,
|
||||
'9' => '_',
|
||||
'a'..'z' => (c as u8 + 1) as char,
|
||||
'_' => 'a',
|
||||
_ => unreachable!(),
|
||||
};
|
||||
s.push(incremented);
|
||||
|
||||
// Pad to original length, like "___0"
|
||||
// Pad to original length
|
||||
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('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");
|
||||
assert_snapshot!(result, @r"
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
|
||||
_, a, b, c, d, e, f, g, h, i
|
||||
j, k, l, m, n, o, p, q, r, s
|
||||
t, u, v, w, x, y, z, _0, _1, _2
|
||||
_3, _4, _5, _6, _7, _8, _9, __, _a, _b
|
||||
t, u, v, w, x, y, z, __, _a, _b
|
||||
_c, _d, _e, _f, _g, _h, _i, _j, _k, _l
|
||||
_m, _n, _o, _p, _q, _r, _s, _t, _u, _v
|
||||
_w, _x, _y, _z, a0, a1, a2, a3, a4, a5
|
||||
a6, a7, a8, a9, a_, aa, ab, ac, ad, ae
|
||||
_w, _x, _y, _z, a_, aa, ab, ac, ad, ae
|
||||
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]
|
||||
fn test_increment_all_len_3() {
|
||||
// Most chars are alphabetic or underscore (26 + 1 = 27).
|
||||
// Last char can include digits (27 + 10 = 37).
|
||||
let num_labels = 37 + 27 * 37 + 27 * 27 * 37; // 1 char + 2 chars + 3 chars
|
||||
let num_labels = 27 + 27 * 27 + 27 * 27 * 27; // 1 char + 2 chars + 3 chars
|
||||
let mut ctr = CompactString::new("");
|
||||
let mut seen = HashSet::with_capacity(num_labels);
|
||||
for _ in 0..num_labels {
|
||||
increment(&mut ctr);
|
||||
assert!(Registry::is_valid_label(&ctr));
|
||||
assert!(!seen.contains(&ctr));
|
||||
assert!(ctr.len() <= 3);
|
||||
seen.insert(ctr.clone());
|
||||
@@ -273,20 +234,20 @@ mod test {
|
||||
let result = results.join("\n");
|
||||
assert_snapshot!(result, @r#"
|
||||
"foo" => foo
|
||||
"ns1~foo" => _foo
|
||||
"ns2~foo" => _foo0
|
||||
"foo.thisloop" => _thisloop
|
||||
"foo.thatloop" => _thatloop
|
||||
"bar.thisloop" => _thisloop0
|
||||
"bar.thatloop" => _thatloop0
|
||||
"foo$1.thisloop" => _thisloop1
|
||||
"foo$1.thatloop" => _thatloop1
|
||||
"bar1" => bar1
|
||||
"bar2" => bar2
|
||||
"bar123" => bar_
|
||||
"BAR123" => BAR_
|
||||
"bar456" => bar_0
|
||||
"BAR456" => BAR_0
|
||||
"ns1~foo" => foo_
|
||||
"ns2~foo" => fooa
|
||||
"foo.thisloop" => thisloop
|
||||
"foo.thatloop" => thatloop
|
||||
"bar.thisloop" => thisloop_
|
||||
"bar.thatloop" => thatloop_
|
||||
"foo$1.thisloop" => thisloopa
|
||||
"foo$1.thatloop" => thatloopa
|
||||
"bar1" => bar_
|
||||
"bar2" => bar__
|
||||
"bar123" => bar_a
|
||||
"BAR123" => BAR_a
|
||||
"bar456" => bar_b
|
||||
"BAR456" => BAR_b
|
||||
"foo2bar" => foo_bar
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -82,12 +82,11 @@ error: message targets not supported for local labels
|
||||
7 | :.local
|
||||
|
|
||||
|
||||
error: local labels with section names not supported
|
||||
=> test.zzt -> Title screen -> @Locals with section names (1,1) -> line 3:2
|
||||
error: local label definitions cannot specify a section name
|
||||
=> test.zzt -> Title screen -> @Local label definitions with section names (1,1) -> line 8:2
|
||||
|
|
||||
1 | @Locals with section names
|
||||
2 | :touch
|
||||
3 | #touch.skip
|
||||
5 | #touch.skip
|
||||
6 | '
|
||||
7 | 'this is not
|
||||
8 | :touch.skip
|
||||
| ^^^^^^^^^^
|
||||
4 | :.skip
|
||||
|
|
||||
|
||||
@@ -9,14 +9,14 @@ expression: board_to_text(board)
|
||||
You have hundreds of gems.
|
||||
#end
|
||||
:lt_
|
||||
#take gems 10 lt_0
|
||||
#take gems 10 lt__
|
||||
#give gems 10
|
||||
You have tens of gems.
|
||||
#end
|
||||
:lt_0
|
||||
#take gems 1 lt1
|
||||
:lt__
|
||||
#take gems 1 lt_a
|
||||
#give gems 1
|
||||
You have at least one gem.
|
||||
#end
|
||||
:lt1
|
||||
:lt_a
|
||||
You don't have any gems!
|
||||
|
||||
@@ -6,50 +6,77 @@ expression: board_to_text(board)
|
||||
'using the same local label ".loop"
|
||||
:run_w
|
||||
#walk w
|
||||
:_loop
|
||||
:loop
|
||||
#try w stop
|
||||
#_loop
|
||||
#loop
|
||||
'
|
||||
:run_e
|
||||
#walk e
|
||||
:_loop0
|
||||
:loop_
|
||||
#try e stop
|
||||
#_loop0
|
||||
#loop_
|
||||
'
|
||||
:stop
|
||||
#walk i
|
||||
---
|
||||
'Using locals before any globals are defined
|
||||
:_loop1
|
||||
#take gems 1 _break
|
||||
#_loop1
|
||||
:_break
|
||||
:loopa
|
||||
#take gems 1 break
|
||||
#loopa
|
||||
:break
|
||||
---
|
||||
'Multiple sections with the same name
|
||||
#end
|
||||
:touch
|
||||
Trying to sell you some ammo...
|
||||
#take gems 10 _skip
|
||||
#take gems 10 skip
|
||||
#give ammo 10
|
||||
:_skip
|
||||
:skip
|
||||
#zap touch
|
||||
#end
|
||||
:touch
|
||||
Trying to sell you some torches...
|
||||
#take gems 15 _skip0
|
||||
#take gems 15 skip_
|
||||
#give torches 5
|
||||
:_skip0
|
||||
:skip_
|
||||
#restore touch
|
||||
---
|
||||
'Multiple locals with the same name
|
||||
#end
|
||||
:repeat3
|
||||
:_z
|
||||
:_z
|
||||
:_z
|
||||
#take gems 1 _skip1
|
||||
:repeat_
|
||||
:z
|
||||
:z
|
||||
:z
|
||||
#take gems 1 skipa
|
||||
#give score 1
|
||||
:_skip1
|
||||
#zap _z
|
||||
#_z
|
||||
:_z
|
||||
:skipa
|
||||
#zap 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
|
||||
:do_stuff
|
||||
#lock
|
||||
:_loop
|
||||
:loop
|
||||
'
|
||||
:_do_stuff
|
||||
:_loop0
|
||||
/i#if blocked rndne _loop0
|
||||
:do_stuff_
|
||||
:loop_
|
||||
/i#if blocked rndne loop_
|
||||
'
|
||||
#take time 1 _break
|
||||
#_loop
|
||||
:_break
|
||||
#take time 1 break
|
||||
#loop
|
||||
:break
|
||||
#unlock
|
||||
|
||||
+7
-5
@@ -1,9 +1,7 @@
|
||||
mod encoding;
|
||||
mod error;
|
||||
mod labels;
|
||||
mod peg;
|
||||
mod preprocess;
|
||||
mod world;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use error::Context as ErrContext;
|
||||
@@ -15,7 +13,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::exit,
|
||||
};
|
||||
use world::World;
|
||||
use zztff::{Program, World};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut input_file = None;
|
||||
@@ -75,7 +73,9 @@ fn main() -> Result<()> {
|
||||
// Codegen: Evaluate all macros
|
||||
for board in &mut world.boards {
|
||||
for stat in &mut board.stats {
|
||||
stat.code = eval_context.eval_program(&stat.code)?;
|
||||
if let Program::Own(code) = &mut stat.program {
|
||||
*code = eval_context.eval_program(code)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,9 @@ fn main() -> Result<()> {
|
||||
let ctx = base_ctx.with_file_path(&input_filename);
|
||||
for (i, board) in world.boards.iter_mut().enumerate() {
|
||||
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
|
||||
|
||||
+1
-1
@@ -207,7 +207,7 @@ impl<'a, T: Clone> Capture<'a, T> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
use peg_macro::grammar;
|
||||
use mzp_peg_macro::grammar;
|
||||
|
||||
use super::ParseState;
|
||||
|
||||
|
||||
-432
@@ -1,432 +0,0 @@
|
||||
use std::{error::Error, fmt::Display};
|
||||
|
||||
use nom::{
|
||||
Err, IResult, Parser,
|
||||
bytes::complete::{tag, take},
|
||||
combinator::fail,
|
||||
error::{ErrorKind, ParseError},
|
||||
multi::count,
|
||||
number::complete::{le_i16, le_u8, le_u16},
|
||||
sequence::tuple,
|
||||
};
|
||||
|
||||
use crate::encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoadError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl<I> ParseError<I> for LoadError {
|
||||
fn from_error_kind(_input: I, kind: ErrorKind) -> Self {
|
||||
Self {
|
||||
message: kind.description().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn append(_input: I, kind: ErrorKind, other: Self) -> Self {
|
||||
Self {
|
||||
message: format!("{}: {:?}", other.message, kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Err<LoadError>> for LoadError {
|
||||
fn from(value: Err<LoadError>) -> Self {
|
||||
let message = match value {
|
||||
Err::Error(e) => e.message,
|
||||
Err::Incomplete(x) => format!("{:?}", x),
|
||||
Err::Failure(e) => e.message,
|
||||
};
|
||||
Self { message }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for LoadError {
|
||||
fn from(value: &str) -> Self {
|
||||
Self {
|
||||
message: value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LoadError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.message.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for LoadError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"description() is deprecated; use Display"
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&dyn Error> {
|
||||
self.source()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct World {
|
||||
pub ammo: i16,
|
||||
pub gems: i16,
|
||||
pub keys: [bool; 7],
|
||||
pub health: i16,
|
||||
pub starting_board: i16,
|
||||
pub torches: i16,
|
||||
pub torch_cycles: i16,
|
||||
pub energizer_cycles: i16,
|
||||
pub score: i16,
|
||||
pub world_name: Vec<u8>,
|
||||
pub flags: [Vec<u8>; 10],
|
||||
pub time: i16,
|
||||
pub time_ticks: i16,
|
||||
pub locked: bool,
|
||||
pub boards: Vec<Board>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Board {
|
||||
pub name: String,
|
||||
pub terrain: Vec<[u8; 2]>,
|
||||
pub max_shots: u8,
|
||||
pub is_dark: bool,
|
||||
pub board_n: u8,
|
||||
pub board_s: u8,
|
||||
pub board_w: u8,
|
||||
pub board_e: u8,
|
||||
pub reenter_when_zapped: bool,
|
||||
pub message: Vec<u8>,
|
||||
pub enter_x: u8,
|
||||
pub enter_y: u8,
|
||||
pub time_limit: i16,
|
||||
pub stats: Vec<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Stat {
|
||||
pub x: u8,
|
||||
pub y: u8,
|
||||
pub x_step: i16,
|
||||
pub y_step: i16,
|
||||
pub cycle: i16,
|
||||
pub p1: u8,
|
||||
pub p2: u8,
|
||||
pub p3: u8,
|
||||
pub follower: i16,
|
||||
pub leader: i16,
|
||||
pub under_element: u8,
|
||||
pub under_color: u8,
|
||||
pub instruction_pointer: i16,
|
||||
pub bind_index: i16,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
impl World {
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<World, LoadError> {
|
||||
let (input, (_, num_boards)) = tuple((tag([0xff, 0xff]), le_i16)).parse(bytes)?;
|
||||
let (input, (ammo, gems, keys)) =
|
||||
tuple((le_i16, le_i16, count(bool_u8, 7))).parse(input)?;
|
||||
let (input, (health, starting_board, torches, torch_cycles, energizer_cycles)) =
|
||||
tuple((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, flags) = count(pstring(20), 10).parse(input)?;
|
||||
let (_input, (time, time_ticks, locked)) = tuple((le_i16, le_i16, bool_u8)).parse(input)?;
|
||||
|
||||
// Rest of header is padding; fast-forward starting from original input
|
||||
let (input, _) = take(512usize).parse(bytes)?;
|
||||
|
||||
// Load boards
|
||||
let num_boards = num_boards as usize + 1;
|
||||
let (_input, chunks) = count(board_slice, num_boards).parse(input)?;
|
||||
let boards: Result<Vec<Board>, LoadError> = chunks
|
||||
.iter()
|
||||
.map(|bytes: &&[u8]| Board::from_bytes(bytes))
|
||||
.collect();
|
||||
let boards = boards?;
|
||||
|
||||
Ok(World {
|
||||
ammo,
|
||||
gems,
|
||||
keys: keys.try_into().unwrap(),
|
||||
health,
|
||||
starting_board,
|
||||
torches,
|
||||
torch_cycles,
|
||||
energizer_cycles,
|
||||
score,
|
||||
world_name,
|
||||
flags: flags.try_into().unwrap(),
|
||||
time,
|
||||
time_ticks,
|
||||
locked,
|
||||
boards,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let mut result = Vec::with_capacity(512);
|
||||
result.push_i16(-1); // file magic: ZZT world
|
||||
result.push_i16(self.boards.len() as i16 - 1);
|
||||
result.push_i16(self.ammo);
|
||||
result.push_i16(self.gems);
|
||||
for key in self.keys {
|
||||
result.push_bool(key);
|
||||
}
|
||||
result.push_i16(self.health);
|
||||
result.push_i16(self.starting_board);
|
||||
result.push_i16(self.torches);
|
||||
result.push_i16(self.torch_cycles);
|
||||
result.push_i16(self.energizer_cycles);
|
||||
result.push_padding(2);
|
||||
result.push_i16(self.score);
|
||||
result.push_string(20, &self.world_name)?;
|
||||
for flag in &self.flags {
|
||||
result.push_string(20, flag)?;
|
||||
}
|
||||
result.push_i16(self.time);
|
||||
result.push_i16(self.time_ticks);
|
||||
result.push_bool(self.locked);
|
||||
result.push_padding(512 - result.len());
|
||||
|
||||
for board in &self.boards {
|
||||
result.extend_from_slice(&board.to_bytes()?);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Board, LoadError> {
|
||||
// Ignore length bytes
|
||||
let (input, _) = le_u16.parse(bytes)?;
|
||||
|
||||
// Read board name
|
||||
let (input, name_bytes) = pstring(50)(input)?;
|
||||
let name = decode_oneline(&name_bytes);
|
||||
|
||||
// Read terrain
|
||||
const NUM_TILES: usize = 60 * 25;
|
||||
let mut input = input;
|
||||
let mut terrain = Vec::with_capacity(NUM_TILES);
|
||||
while terrain.len() < NUM_TILES {
|
||||
let (next_input, (count, element, color)) = tuple((le_u8, le_u8, le_u8))(input)?;
|
||||
input = next_input;
|
||||
let count: u32 = if count == 0 { 256 } else { count.into() };
|
||||
for _ in 0..count {
|
||||
terrain.push([element, color]);
|
||||
if terrain.len() > NUM_TILES {
|
||||
return Err("too many tiles of board terrain".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read board info
|
||||
let (input, (max_shots, is_dark)) = tuple((le_u8, bool_u8))(input)?;
|
||||
let (input, (board_n, board_s, board_w, board_e)) =
|
||||
tuple((le_u8, le_u8, le_u8, le_u8))(input)?;
|
||||
let (input, (reenter_when_zapped, message)) = tuple((bool_u8, pstring(58)))(input)?;
|
||||
let (input, (enter_x, enter_y, time_limit)) = tuple((le_u8, le_u8, le_i16))(input)?;
|
||||
let (input, _) = take(16usize)(input)?;
|
||||
|
||||
// Read stats
|
||||
let (input, num_stats) = le_i16(input)?;
|
||||
let num_stats = num_stats + 1;
|
||||
if num_stats < 0 {
|
||||
return Err("cannot have a negative number of stats".into());
|
||||
}
|
||||
let (_input, stats) = count(Stat::from_bytes, num_stats as usize)(input)?;
|
||||
|
||||
Ok(Board {
|
||||
name,
|
||||
terrain,
|
||||
max_shots,
|
||||
is_dark,
|
||||
board_n,
|
||||
board_s,
|
||||
board_e,
|
||||
board_w,
|
||||
reenter_when_zapped,
|
||||
message,
|
||||
enter_x,
|
||||
enter_y,
|
||||
time_limit,
|
||||
stats,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, &'static str> {
|
||||
let mut result = vec![];
|
||||
result.push_padding(2); // reserve space for board size
|
||||
let name_bytes = encode_oneline(&self.name).unwrap();
|
||||
result.push_string(50, &name_bytes)?;
|
||||
|
||||
// Encode terrain
|
||||
if self.terrain.len() != 1500 {
|
||||
return Err("invalid number of tiles for board terrain");
|
||||
}
|
||||
let mut iter = self.terrain.iter().peekable();
|
||||
while let Some(tile) = iter.next() {
|
||||
let mut count = 1;
|
||||
while count < 255 && iter.peek().map_or(false, |&next_tile| next_tile == tile) {
|
||||
count += 1;
|
||||
iter.next();
|
||||
}
|
||||
result.push(count);
|
||||
result.extend_from_slice(tile);
|
||||
}
|
||||
|
||||
// Board info
|
||||
result.push(self.max_shots);
|
||||
result.push_bool(self.is_dark);
|
||||
result.push(self.board_n);
|
||||
result.push(self.board_s);
|
||||
result.push(self.board_w);
|
||||
result.push(self.board_e);
|
||||
result.push_bool(self.reenter_when_zapped);
|
||||
result.push_string(58, &self.message)?;
|
||||
result.push(self.enter_x);
|
||||
result.push(self.enter_y);
|
||||
result.push_i16(self.time_limit);
|
||||
result.push_padding(16);
|
||||
|
||||
// Stats
|
||||
let num_stats: i16 = (self.stats.len() - 1)
|
||||
.try_into()
|
||||
.map_err(|_| "invalid length for stats")?;
|
||||
result.push_i16(num_stats);
|
||||
for stat in &self.stats {
|
||||
result.extend_from_slice(&stat.to_bytes());
|
||||
}
|
||||
|
||||
// Fix up board size
|
||||
let size: u16 = (result.len() - 2)
|
||||
.try_into()
|
||||
.map_err(|_| "too many bytes of board data")?;
|
||||
result.splice(0..2, size.to_le_bytes());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Stat {
|
||||
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, (cycle, p1, p2, p3)) = tuple((le_i16, le_u8, le_u8, le_u8))(input)?;
|
||||
let (input, (follower, leader)) = tuple((le_i16, le_i16))(input)?;
|
||||
let (input, (under_element, under_color)) = tuple((le_u8, le_u8))(input)?;
|
||||
let (input, _) = take(4usize)(input)?;
|
||||
let (input, (instruction_pointer, length)) = tuple((le_i16, le_i16))(input)?;
|
||||
let (input, _) = take(8usize)(input)?;
|
||||
let (input, code_bytes) = take(0.max(length) as usize)(input)?;
|
||||
let code = decode_multiline(&code_bytes);
|
||||
Ok((
|
||||
input,
|
||||
Stat {
|
||||
x,
|
||||
y,
|
||||
x_step,
|
||||
y_step,
|
||||
follower,
|
||||
leader,
|
||||
cycle,
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
under_element,
|
||||
under_color,
|
||||
instruction_pointer,
|
||||
bind_index: 0.min(length),
|
||||
code,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut result = vec![];
|
||||
result.push(self.x);
|
||||
result.push(self.y);
|
||||
result.push_i16(self.x_step);
|
||||
result.push_i16(self.y_step);
|
||||
result.push_i16(self.cycle);
|
||||
result.push(self.p1);
|
||||
result.push(self.p2);
|
||||
result.push(self.p3);
|
||||
result.push_i16(self.follower);
|
||||
result.push_i16(self.leader);
|
||||
result.push(self.under_element);
|
||||
result.push(self.under_color);
|
||||
result.push_padding(4);
|
||||
result.push_i16(self.instruction_pointer);
|
||||
// TODO: more safety around valid bind-indexes (positive? negative?)
|
||||
result.push_i16(if self.bind_index < 0 {
|
||||
self.bind_index
|
||||
} else {
|
||||
self.code.len() as i16
|
||||
});
|
||||
result.push_padding(8);
|
||||
if self.bind_index >= 0 {
|
||||
// TODO: more safety around bind-index XOR code
|
||||
let code_bytes = encode_multiline(&self.code).unwrap();
|
||||
result.extend_from_slice(&code_bytes);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn bool_u8(input: &[u8]) -> IResult<&[u8], bool, LoadError> {
|
||||
let (input, byte) = le_u8(input)?;
|
||||
Ok((input, byte != 0))
|
||||
}
|
||||
|
||||
fn pstring(cap: u8) -> impl Fn(&[u8]) -> IResult<&[u8], Vec<u8>, LoadError> {
|
||||
move |input: &[u8]| -> IResult<&[u8], Vec<u8>, LoadError> {
|
||||
let (input, len) = le_u8(input)?;
|
||||
if len >= cap {
|
||||
return fail(input);
|
||||
}
|
||||
let (input, data) = take(len)(input)?;
|
||||
let (input, _) = take(cap - len)(input)?;
|
||||
Ok((input, data.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
fn board_slice(bytes: &[u8]) -> IResult<&[u8], &[u8], LoadError> {
|
||||
let (_, size) = le_u16.parse(bytes)?;
|
||||
take(size + 2).parse(bytes)
|
||||
}
|
||||
|
||||
trait SerializationHelpers {
|
||||
fn push_bool(&mut self, value: bool);
|
||||
fn push_i16(&mut self, value: i16);
|
||||
fn push_string(&mut self, cap: u8, value: &[u8]) -> Result<(), &'static str>;
|
||||
fn push_padding(&mut self, size: usize);
|
||||
}
|
||||
|
||||
impl SerializationHelpers for Vec<u8> {
|
||||
fn push_bool(&mut self, value: bool) {
|
||||
self.push(if value { 1 } else { 0 });
|
||||
}
|
||||
|
||||
fn push_i16(&mut self, value: i16) {
|
||||
self.extend(value.to_le_bytes());
|
||||
}
|
||||
|
||||
fn push_string(&mut self, cap: u8, value: &[u8]) -> Result<(), &'static str> {
|
||||
if value.len() > cap as usize {
|
||||
return Err("string too long");
|
||||
}
|
||||
self.push(value.len() as u8);
|
||||
self.extend_from_slice(value);
|
||||
self.push_padding(cap as usize - value.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_padding(&mut self, size: usize) {
|
||||
self.resize(self.len() + size, 0);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,11 @@
|
||||
:@
|
||||
:.local
|
||||
---
|
||||
@Locals with section names
|
||||
@Local label definitions with section names
|
||||
'these are okay
|
||||
:touch
|
||||
#touch.skip
|
||||
:.skip
|
||||
#touch.skip
|
||||
'
|
||||
'this is not
|
||||
:touch.skip
|
||||
|
||||
+27
-1
@@ -49,4 +49,30 @@ Trying to sell you some torches...
|
||||
#zap .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