Canonicalize floating zero when parsing text tags

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Florian ERNSTandCodex committed 2026-11-06 16:57:32 +01:00
1 parent 41f822b659
commit 1d800a7655
4 files changed
+287 -7

No files matched your search

+82 -2
View File
@@ -257,6 +257,7 @@ impl IntLit {
}
}
/// `FloatTag.valueOf` / `DoubleTag.valueOf`: either zero sign uses the zero tag.
fn float_tag(text: &str, ty: Option<TypeSuffix>, state: &mut State<SV>) -> Option<Tag> {
match ty {
Some(TypeSuffix::Float) => {
@@ -265,7 +266,7 @@ fn float_tag(text: &str, ty: Option<TypeSuffix>, state: &mut State<SV>) -> Optio
state.store_at_mark(delayed("snbt.parser.infinity_not_allowed"));
return None;
}
Some(Tag::Float(v.to_bits()))
Some(Tag::Float(if v == 0.0 { 0 } else { v.to_bits() }))
}
None | Some(TypeSuffix::Double) => {
let v: f64 = text.parse().unwrap_or(f64::NAN);
@@ -273,7 +274,7 @@ fn float_tag(text: &str, ty: Option<TypeSuffix>, state: &mut State<SV>) -> Optio
state.store_at_mark(delayed("snbt.parser.infinity_not_allowed"));
return None;
}
Some(Tag::Double(v.to_bits()))
Some(Tag::Double(if v == 0.0 { 0 } else { v.to_bits() }))
}
_ => {
state.store_at_mark(delayed("snbt.parser.expected_float_type"));
@@ -1029,3 +1030,82 @@ mod tests {
assert!(parse_fully(&item).is_ok());
}
}
#[cfg(test)]
mod floating_zero_tests {
use super::*;
use horizon_world::nbt::{Limits, read_network};
fn cases() -> Vec<(&'static str, Tag)> {
vec![
("0.0f", Tag::Float(0)),
("-0.0f", Tag::Float(0)),
("0e0f", Tag::Float(0)),
("-0e0f", Tag::Float(0)),
("0.0d", Tag::Double(0)),
("-0.0d", Tag::Double(0)),
("0e0d", Tag::Double(0)),
("-0e0d", Tag::Double(0)),
("0.0", Tag::Double(0)),
("-0.0", Tag::Double(0)),
("0e0", Tag::Double(0)),
("-0e0", Tag::Double(0)),
("1.25f", Tag::Float(0x3fa0_0000)),
("-1.25f", Tag::Float(0xbfa0_0000)),
("1.25d", Tag::Double(0x3ff4_0000_0000_0000)),
("-1.25d", Tag::Double(0xbff4_0000_0000_0000)),
("1e-45f", Tag::Float(1)),
("-1e-45f", Tag::Float(0x8000_0001)),
("5e-324d", Tag::Double(1)),
("-5e-324d", Tag::Double(0x8000_0000_0000_0001)),
("-0", Tag::Int(0)),
]
}
#[test]
fn command_text_keeps_tag_factory_zero_and_nonzero_bits() {
for (text, expected) in cases() {
assert_eq!(parse_fully(text).unwrap(), expected, "{text}");
}
}
#[test]
fn world_text_keeps_tag_factory_zero_and_nonzero_bits() {
for (text, expected) in cases() {
let (actual, used) = horizon_world::nbt::parse_snbt(text).unwrap();
assert_eq!(used, text.len(), "{text}");
assert_eq!(actual, expected, "{text}");
}
}
#[test]
fn binary_decode_already_canonicalizes_only_zero() {
for (bits, expected) in [
(0u32, 0),
(0x8000_0000, 0),
(1, 1),
(0x8000_0001, 0x8000_0001),
(0x3fa0_0000, 0x3fa0_0000),
(0xbfa0_0000, 0xbfa0_0000),
] {
let mut bytes = vec![5];
bytes.extend(bits.to_be_bytes());
assert_eq!(
read_network(&bytes, Limits::default()).unwrap(),
(Tag::Float(expected), bytes.len())
);
}
for (bits, expected) in [
(0u64, 0),
(0x8000_0000_0000_0000, 0),
(1, 1),
(0x8000_0000_0000_0001, 0x8000_0000_0000_0001),
(0x3ff4_0000_0000_0000, 0x3ff4_0000_0000_0000),
(0xbff4_0000_0000_0000, 0xbff4_0000_0000_0000),
] {
let mut bytes = vec![6];
bytes.extend(bits.to_be_bytes());
assert_eq!(
read_network(&bytes, Limits::default()).unwrap(),
(Tag::Double(expected), bytes.len())
);
}
}
}
+13 -5
View File
@@ -836,6 +836,7 @@ impl Snbt<'_> {
})
}
/// A bare word: a typed number, a boolean, or else a string.
/// `FloatTag.valueOf` / `DoubleTag.valueOf` canonicalize either zero sign.
fn scalar(word: &str) -> Tag {
match word {
"true" => return Tag::Byte(1),
@@ -859,11 +860,18 @@ impl Snbt<'_> {
Some(b'b') => body.parse::<i8>().ok().map(Tag::Byte),
Some(b's') => body.parse::<i16>().ok().map(Tag::Short),
Some(b'l') => body.parse::<i64>().ok().map(Tag::Long),
Some(b'f') => body.parse::<f32>().ok().map(|v| Tag::Float(v.to_bits())),
Some(_) => body.parse::<f64>().ok().map(|v| Tag::Double(v.to_bits())),
None if body.contains('.') || body.contains('e') || body.contains('E') => {
body.parse::<f64>().ok().map(|v| Tag::Double(v.to_bits()))
}
Some(b'f') => body
.parse::<f32>()
.ok()
.map(|v| Tag::Float(if v == 0.0 { 0 } else { v.to_bits() })),
Some(_) => body
.parse::<f64>()
.ok()
.map(|v| Tag::Double(if v == 0.0 { 0 } else { v.to_bits() })),
None if body.contains('.') || body.contains('e') || body.contains('E') => body
.parse::<f64>()
.ok()
.map(|v| Tag::Double(if v == 0.0 { 0 } else { v.to_bits() })),
None => body.parse::<i32>().ok().map(Tag::Int),
};
parsed.unwrap_or_else(|| Tag::String(JavaString::from(word)))
+88
View File
@@ -0,0 +1,88 @@
# Text floating-zero tag creation (H190)
Frozen source `41f822b659789780f58d32c63ddfb6887ea1b8bf`. Own actual Game/fresh pinned Java26.3 via `tools/test_text_floating_zero_parity.py`. All120 complete command records compare exact typed scalar text, styles, callbacks, copy/equality/no-write results and fresh result/success sentinels without masks/tolerances. Five separately declared raw traces retain full storage compounds and absolute startup clocks; no whole-compound ordering equality is claimed.
The20-field matrix includes Float/Double ±zero in suffix/exponent forms, ordinary/exponent unsuffixed doubles, nonzero ±1.25 in both types and signed subnormals1e-40f/1e-310d. Storage anchors independently prove real source/copy existence. Each initial/copy field is read exactly and checked against its typed expected tag. A duplicate copy is a no-write control; setting the equivalent canonical compound must also report no change. Loaded floor/player identity and actual relative-clock0 remain strict. Original negative literals enter the real commands unchanged; the second positive-zero compound is a separate equality test, not input/output normalization.
Untouched Game: **93/120**; fixed Game: **120/120**, same program and byte-identical fresh Java reference; all control admissions pass. The27 residuals below are six negative-zero variants' source/copy reads and typed predicates, plus three canonical no-write action/store observations. This proves semantic tag equality, not a formatter-only difference.
## Exact untouched residuals
| Label | Java expected | Untouched Game actual | Callback expected / actual |
|---|---|---|---|
| f_plain_neg-source | `Storage h190:source has the following contents: 0.0f` | `Storage h190:source has the following contents: -0.0f` | `[[True, 0]]` / `[[True, 0]]` |
| f_plain_neg-source-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| f_exp_neg-source | `Storage h190:source has the following contents: 0.0f` | `Storage h190:source has the following contents: -0.0f` | `[[True, 0]]` / `[[True, 0]]` |
| f_exp_neg-source-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| d_plain_neg-source | `Storage h190:source has the following contents: 0.0d` | `Storage h190:source has the following contents: -0.0d` | `[[True, 0]]` / `[[True, 0]]` |
| d_plain_neg-source-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| d_exp_neg-source | `Storage h190:source has the following contents: 0.0d` | `Storage h190:source has the following contents: -0.0d` | `[[True, 0]]` / `[[True, 0]]` |
| d_exp_neg-source-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| bare_plain_neg-source | `Storage h190:source has the following contents: 0.0d` | `Storage h190:source has the following contents: -0.0d` | `[[True, 0]]` / `[[True, 0]]` |
| bare_plain_neg-source-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| bare_exp_neg-source | `Storage h190:source has the following contents: 0.0d` | `Storage h190:source has the following contents: -0.0d` | `[[True, 0]]` / `[[True, 0]]` |
| bare_exp_neg-source-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| f_plain_neg-copy | `Storage h190:copy has the following contents: 0.0f` | `Storage h190:copy has the following contents: -0.0f` | `[[True, 0]]` / `[[True, 0]]` |
| f_plain_neg-copy-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| f_exp_neg-copy | `Storage h190:copy has the following contents: 0.0f` | `Storage h190:copy has the following contents: -0.0f` | `[[True, 0]]` / `[[True, 0]]` |
| f_exp_neg-copy-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| d_plain_neg-copy | `Storage h190:copy has the following contents: 0.0d` | `Storage h190:copy has the following contents: -0.0d` | `[[True, 0]]` / `[[True, 0]]` |
| d_plain_neg-copy-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| d_exp_neg-copy | `Storage h190:copy has the following contents: 0.0d` | `Storage h190:copy has the following contents: -0.0d` | `[[True, 0]]` / `[[True, 0]]` |
| d_exp_neg-copy-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| bare_plain_neg-copy | `Storage h190:copy has the following contents: 0.0d` | `Storage h190:copy has the following contents: -0.0d` | `[[True, 0]]` / `[[True, 0]]` |
| bare_plain_neg-copy-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| bare_exp_neg-copy | `Storage h190:copy has the following contents: 0.0d` | `Storage h190:copy has the following contents: -0.0d` | `[[True, 0]]` / `[[True, 0]]` |
| bare_exp_neg-copy-type | `Test passed. Count: 1` | `Test failed` | `[[True, 1]]` / `[[False, 0]]` |
| canonical-no-write | `Nothing changed. The specified properties already have these values` | `Modified storage h190:source` | `[[False, 0]]` / `[[True, 1]]` |
| canonical-no-write-result | `result has 0 [h190]` | `result has 1 [h190]` | `[[True, 0]]` / `[[True, 1]]` |
| canonical-no-write-success | `success has 0 [h190]` | `success has 1 [h190]` | `[[True, 0]]` / `[[True, 1]]` |
## Source and scope
Pinned FloatTag.valueOf and DoubleTag.valueOf compare numerically with zero and return ZERO for either sign. Their ZERO initializers use positive fconst_0/dconst_0. Both text parser paths directly retained parsed sign bits. The repair canonicalizes only numeric zero at their Float/Double construction sites; nonzero and subnormal bits pass through unchanged.
Resource-free units exercise the real modern command parser and world text parser independently with exact bit/type expectations, including minimum signed subnormals and an unchanged Int control. Both fail old at -0.0f (0x80000000 instead of0). A separate binary-decode control already passes old and covers both zero signs, nonzero signs and signed minimum subnormals. Binary read/write code is unchanged.
No primitive formatting, arithmetic, global numeric coercion, grammar, compound order or data-command route change. H189 modern execution parsing remains independent; its grammar/routing changes must preserve these factory guards. Historical52-route and358-format diagnostics remain historical/unmodified, not this own current denominator. No root code/resource/reference payload is imported.
## Reproduce and retain
```sh
python3 tools/test_text_floating_zero_parity.py --reference-only --work-dir /tmp/zero-java/run
python3 tools/test_text_floating_zero_parity.py --binary /absolute/path/to/horizon-client --reference /tmp/zero-java/run/vanilla.jsonl --work-dir /tmp/zero-game/run
```
Fresh output directories are required. Complete scripts/declarations, references, raw JSONL/logs, immutable binaries and hashes are retained under `.local/h190/`.
- Program SHA256: `acdb6aab09c2c1d7865b4bb932c894fe0f218d1ed3dc1020e3dce69f6ab68284`
- Fresh Java reference SHA256: `f6ae6637b0352fea2417d80ca87ef59697c9064b189beb80932694004701ff1d`
- Untouched Game SHA256: `e45a7e8023e21869d10c1b010f3c3617bb092f62e3b941a94bbd986619d1e131`
## Raw diagnostic limits
All120 typed/equality records match. The five separately declared raw records retain compound display-order and absolute startup-clock differences. Complete expected/actual text follows; full styles/callbacks remain in retained logs. No formatting or compound-history repair, raw-clock normalization or whole-body equality claim.
```text
/execute store result score start h190 run time query gametime
JAVA: The game time is 301 tick(s)
GAME: The game time is 0 tick(s)
/data get storage h190:source
JAVA: Storage h190:source has the following contents: {anchor: 1b, samples: {f_sub_neg: -1.0E-40f, bare_plain_pos: 0.0d, f_sub_pos: 1.0E-40f, d_sub_neg: -1.0E-310d, f_value_neg: -1.25f, d_plain_neg: 0.0d, d_exp_pos: 0.0d, d_sub_pos: 1.0E-310d, f_value_pos: 1.25f, d_plain_pos: 0.0d, d_exp_neg: 0.0d, f_exp_neg: 0.0f, f_plain_neg: 0.0f, d_value_pos: 1.25d, f_plain_pos: 0.0f, bare_plain_neg: 0.0d, bare_exp_pos: 0.0d, f_exp_pos: 0.0f, bare_exp_neg: 0.0d, d_value_neg: -1.25d}}
GAME: Storage h190:source has the following contents: {anchor: 1b, samples: {f_sub_neg: -1.0E-40f, bare_plain_pos: 0.0d, f_sub_pos: 1.0E-40f, d_sub_neg: -1.0E-310d, f_value_neg: -1.25f, d_exp_pos: 0.0d, d_plain_neg: 0.0d, d_sub_pos: 1.0E-310d, f_value_pos: 1.25f, d_exp_neg: 0.0d, d_plain_pos: 0.0d, f_exp_neg: 0.0f, f_plain_neg: 0.0f, d_value_pos: 1.25d, f_plain_pos: 0.0f, bare_plain_neg: 0.0d, bare_exp_pos: 0.0d, f_exp_pos: 0.0f, bare_exp_neg: 0.0d, d_value_neg: -1.25d}}
/data get storage h190:source
JAVA: Storage h190:source has the following contents: {anchor: 1b, samples: {f_sub_neg: -1.0E-40f, bare_plain_pos: 0.0d, f_sub_pos: 1.0E-40f, d_sub_neg: -1.0E-310d, f_value_neg: -1.25f, d_plain_neg: 0.0d, d_exp_pos: 0.0d, d_sub_pos: 1.0E-310d, f_value_pos: 1.25f, d_plain_pos: 0.0d, d_exp_neg: 0.0d, f_exp_neg: 0.0f, f_plain_neg: 0.0f, d_value_pos: 1.25d, f_plain_pos: 0.0f, bare_plain_neg: 0.0d, bare_exp_pos: 0.0d, f_exp_pos: 0.0f, bare_exp_neg: 0.0d, d_value_neg: -1.25d}}
GAME: Storage h190:source has the following contents: {anchor: 1b, samples: {f_sub_neg: -1.0E-40f, bare_plain_pos: 0.0d, f_sub_pos: 1.0E-40f, d_sub_neg: -1.0E-310d, f_value_neg: -1.25f, d_exp_pos: 0.0d, d_plain_neg: 0.0d, d_sub_pos: 1.0E-310d, f_value_pos: 1.25f, d_exp_neg: 0.0d, d_plain_pos: 0.0d, f_exp_neg: 0.0f, f_plain_neg: 0.0f, d_value_pos: 1.25d, f_plain_pos: 0.0f, bare_plain_neg: 0.0d, bare_exp_pos: 0.0d, f_exp_pos: 0.0f, bare_exp_neg: 0.0d, d_value_neg: -1.25d}}
/data get storage h190:copy
JAVA: Storage h190:copy has the following contents: {anchor: 1b, samples: {f_sub_neg: -1.0E-40f, bare_plain_pos: 0.0d, f_sub_pos: 1.0E-40f, d_sub_neg: -1.0E-310d, f_value_neg: -1.25f, d_plain_neg: 0.0d, d_exp_pos: 0.0d, d_sub_pos: 1.0E-310d, f_value_pos: 1.25f, d_plain_pos: 0.0d, d_exp_neg: 0.0d, f_exp_neg: 0.0f, f_plain_neg: 0.0f, d_value_pos: 1.25d, f_plain_pos: 0.0f, bare_plain_neg: 0.0d, bare_exp_pos: 0.0d, f_exp_pos: 0.0f, bare_exp_neg: 0.0d, d_value_neg: -1.25d}}
GAME: Storage h190:copy has the following contents: {anchor: 1b, samples: {f_sub_neg: -1.0E-40f, bare_plain_pos: 0.0d, f_sub_pos: 1.0E-40f, d_sub_neg: -1.0E-310d, f_value_neg: -1.25f, d_exp_pos: 0.0d, d_plain_neg: 0.0d, d_sub_pos: 1.0E-310d, f_value_pos: 1.25f, d_exp_neg: 0.0d, d_plain_pos: 0.0d, f_exp_neg: 0.0f, f_plain_neg: 0.0f, d_value_pos: 1.25d, f_plain_pos: 0.0f, bare_plain_neg: 0.0d, bare_exp_pos: 0.0d, f_exp_pos: 0.0f, bare_exp_neg: 0.0d, d_value_neg: -1.25d}}
/execute store result score end h190 run time query gametime
JAVA: The game time is 301 tick(s)
GAME: The game time is 0 tick(s)
```
## Local validation
Required pr_ready ran once final: **516 Rust tests passed**, including both text-path bit/type regressions and the already-correct binary controls. Rustfmt, clippy (including server features), doc tests and public-tree checks passed. Overall exit1 is the same three pre-existing macOS optional-release-archive Python subtests (masks0/1 musl[@], mask2 arm64[@] unbound Bash arrays);708 ran, one skipped. The436 broad affected harnesses were listed for CI by --quick; the focused actual Game pair was run locally.
Ledger refresh initially found no default jar; that failed tooling attempt is retained. Rebuilding with the actual pinned server-game.jar succeeds, but the indexer unconditionally excludes valueOf methods (tools/port_ledger.py:612). Its four stale-citation warnings are therefore false positives: both factories and positive-zero initializers are directly confirmed in that jar’s javap output. No ledger-engine fix is included. An unrelated rest-010 map-check read_loop row is also stale; the attempted note was reverted, leaving mapping files unchanged.
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Floating zero factories through actual typed storage commands."""
import hashlib
import json
from pathlib import Path
import sys
import tempfile
import test_command_exec_parity as driver
CASES = [('f_plain_pos', '0.0f', '0.0f', False), ('f_plain_neg', '-0.0f', '0.0f', True), ('f_exp_pos', '0e0f', '0.0f', False), ('f_exp_neg', '-0e0f', '0.0f', True), ('d_plain_pos', '0.0d', '0.0d', False), ('d_plain_neg', '-0.0d', '0.0d', True), ('d_exp_pos', '0e0d', '0.0d', False), ('d_exp_neg', '-0e0d', '0.0d', True), ('bare_plain_pos', '0.0', '0.0d', False), ('bare_plain_neg', '-0.0', '0.0d', True), ('bare_exp_pos', '0e0', '0.0d', False), ('bare_exp_neg', '-0e0', '0.0d', True), ('f_value_pos', '1.25f', '1.25f', False), ('f_value_neg', '-1.25f', '-1.25f', False), ('d_value_pos', '1.25d', '1.25d', False), ('d_value_neg', '-1.25d', '-1.25d', False), ('f_sub_pos', '1.0e-40f', '1.0e-40f', False), ('f_sub_neg', '-1.0e-40f', '-1.0e-40f', False), ('d_sub_pos', '1.0e-310d', '1.0e-310d', False), ('d_sub_neg', '-1.0e-310d', '-1.0e-310d', False)]
def program():
ops=[]
def cmd(text,label,result=None,admission=True,diagnostic=False):
op=dict(op='cmd',cmd=text,label=label,admission=admission)
if result is not None:op['expect_result']=result
if diagnostic:op.update(record=False,trace=True)
ops.append(op)
def stored(command,label,admission=False):
cmd('/scoreboard players set result h190 -99',label+'-reset-result',-99)
cmd('/scoreboard players set success h190 -99',label+'-reset-success',-99)
cmd('/execute store result score result h190 store success score success h190 run '+command,label,admission=admission)
cmd('/scoreboard players get result h190',label+'-result',admission=admission)
cmd('/scoreboard players get success h190',label+'-success',admission=admission)
cmd('/tick freeze','freeze')
cmd('/gamerule spawn_mobs false','no-spawn')
cmd('/gamerule advance_time false','no-daylight')
cmd('/forceload add 0 0','force',1)
ops.append(dict(op='settle',required_chunks=[[0,0]]))
cmd('/fill 0 199 0 15 199 15 stone','floor',256)
cmd('/tp @s 14.5 200 14.5 0 0','player-position',1)
cmd('/gamemode creative @s','player-mode')
cmd('/execute if loaded 14 200 14','loaded',1)
cmd('/execute if block 14 199 14 stone','support',1)
cmd('/execute if entity @s[type=player,name=Player,nbt={Health:20f,playerGameType:1,Pos:[14.5d,200d,14.5d]}]','player',1)
cmd('/data get entity @s UUID','player-uuid')
cmd('/scoreboard objectives add h190 dummy','objective')
cmd('/execute store result score start h190 run time query gametime','clock-start',diagnostic=True)
for storage in ['source','copy']:
cmd('/data merge storage h190:'+storage+' {anchor:1b}',storage+'-anchor',1)
cmd('/execute if data storage h190:'+storage+' {anchor:1b}',storage+'-exists',1)
original='{'+','.join(key+':'+text for key,text,canonical,target in CASES)+'}'
canonical='{'+','.join(key+':'+value for key,text,value,target in CASES)+'}'
stored('data modify storage h190:source samples set value '+original,'initial-write',True)
cmd('/data get storage h190:source','initial-full',diagnostic=True)
for key,text,value,target in CASES:
cmd('/data get storage h190:source samples.'+key,key+'-source',admission=not target)
cmd('/execute if data storage h190:source {samples:{'+key+':'+value+'}}',key+'-source-type',None if target else 1,admission=not target)
stored('data modify storage h190:copy samples set from storage h190:source samples','copy-write',True)
for key,text,value,target in CASES:
cmd('/data get storage h190:copy samples.'+key,key+'-copy',admission=not target)
cmd('/execute if data storage h190:copy {samples:{'+key+':'+value+'}}',key+'-copy-type',None if target else 1,admission=not target)
stored('data modify storage h190:copy samples set from storage h190:source samples','duplicate-copy',True)
stored('data modify storage h190:source samples set value '+canonical,'canonical-no-write')
cmd('/data get storage h190:source','source-final-full',diagnostic=True)
cmd('/data get storage h190:copy','copy-final-full',diagnostic=True)
for storage in ['source','copy']:
cmd('/execute if data storage h190:'+storage+' {anchor:1b}',storage+'-retained',1)
cmd('/execute store result score end h190 run time query gametime','clock-end',diagnostic=True)
cmd('/scoreboard players operation end h190 -= start h190','clock-delta-operation',0)
cmd('/scoreboard players get end h190','clock-delta',0)
return ops
def main():
reference = None
if '--reference' in sys.argv:
i = sys.argv.index('--reference'); reference = Path(sys.argv[i+1]).resolve()
del sys.argv[i:i+2]
if '--work-dir' not in sys.argv:
parent = Path(tempfile.mkdtemp(prefix='floating-zero-', dir=driver.lab.LOCAL))
sys.argv += ['--work-dir', str(parent / 'run')]
build = Path(sys.argv[sys.argv.index('--work-dir')+1]).resolve()
assert not build.exists(), 'preserve attempts; choose a fresh directory'
build.mkdir(parents=True)
ops = program(); recorded = [op for op in ops if op['op']=='cmd' and op.get('record', True)]
(build/'declaration.json').write_text(json.dumps(dict(
script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), records=len(recorded),
cases=CASES, labels=[op['label'] for op in recorded],
raw_diagnostics=[op['label'] for op in ops if op.get('trace')],
comparison='complete records, no masks/tolerances; bodies and absolute clocks are separate raw traces'), indent=2)+'\n')
expected_bytes = reference.read_bytes() if reference else None
driver.corpus_module.groups = lambda: [('Text floating zero', ops)]
sys.argv.append('--allow-gaps'); driver.main()
java_bytes = (build/'vanilla.jsonl').read_bytes()
if expected_bytes is not None:
assert java_bytes==expected_bytes and reference.read_bytes()==expected_bytes, 'fresh Java reference changed'
java = [json.loads(s) for s in java_bytes.splitlines()]
if '--reference-only' in sys.argv: return
native = [json.loads(s) for s in (build/'rust.jsonl').read_bytes().splitlines()]
assert len(recorded)==len(java)==len(native)
residuals = [dict(label=op['label'], expected=a, actual=b) for op,a,b in zip(recorded,java,native) if a!=b]
report = dict(records=len(recorded), passed=len(recorded)-len(residuals), residuals=residuals,
reference_sha256=hashlib.sha256(java_bytes).hexdigest())
(build/'strict-report.json').write_text(json.dumps(report, indent=2)+'\n')
for op,a,b in zip(recorded,java,native):
if op['admission']: assert a==b, ('admission',op,a,b)
print(f'Strict records: {report["passed"]}/{report["records"]}')
if residuals: raise SystemExit(1)
if __name__ == '__main__':
main()