What
The PXF lexer cannot read fractional or µs duration literals — 1.5ms, 1.234567ms, 1.5h, 2µs, 312.5µs, 1h30m0.5s — even though draft-trendvidia-protowire-01 §3.3 admits all of them:
duration-segment = 1*DIGIT [ "." 1*DIGIT ] time-unit
time-unit = "ns" / "us" / micro-us / "ms" / "s" / "m" / "h"
micro-us = %xC2.B5 %x73 ; UTF-8 of "µs"
and §3.10 lists 1.5h and 2µs as examples. Two defects, both inherited from the Go reference lexer (fixed there in trendvidia/protowire-go#76 for trendvidia/protowire-go#75):
- Ordering. After the integer digits,
lexNumber checks for . / e / E (→ float) before it checks for a time unit, so 1.5ms tokenises as FLOAT 1.5 followed by IDENT ms, and the decoder then reports the message-typed Duration field as receiving a scalar (expected '{' or this port's equivalent).
- ASCII-only unit scan. The duration scan accepts only digits and
a-z, so the two-byte UTF-8 µ (U+00B5, C2 B5) never joins the token and 2µs becomes INT 2 plus a stray byte.
Why it matters
These are exactly the forms the Go, Rust, C++ and TypeScript encoders write for a google.protobuf.Duration that is not a whole multiple of its largest unit — i.e. every measured latency — because they all mirror time.Duration.String() (1.234567ms, 312.5µs, 1h30m0.5s, -2.5s). A document produced by any of those ports with such a value cannot be read by this one.
Where
src/pxf/lexer.cc:
- ~L521–525:
if (... Peek() == '.' || Peek() == 'e' || Peek() == 'E') return LexFloat(...) runs before IsDurationUnit(Peek()).
LexDuration (~L560) scans IsDigit || IsLowerAlpha only; detail::ParseDuration (src/detail/duration.cc) already understands C2 B5 and fractions, so validation is not the blocker, tokenisation is.
Encoder: src/detail/duration.cc (~L164–177) writes %lldµs and %lld.%sµs, so this port cannot read its own output for measured values — same self-inflicted shape as Go.
protowire-python wraps this library (_protowire via nanobind, protowire_pxf target) and inherits both the bug and the fix.
- In the number path: after the leading digits, consume an optional fraction (
. followed by ≥1 digit) first; then, if a unit start follows (ASCII h m s n u, or the byte pair C2 B5), take the duration branch; otherwise fall through to float (fraction consumed, or exponent, or bare trailing .), else INT.
- In the duration scan: also accept
. when followed by a digit, and the C2 B5 pair; keep validating the whole literal with the port's Go-duration parser afterwards.
- Deliberately unchanged in Go, worth pinning here too:
1.5 → FLOAT; 1.5x → FLOAT + IDENT x; 1.ms → FLOAT 1. + IDENT (no digit after ., so not a duration-segment); 1.5e3ms → FLOAT + IDENT (an exponent is not a unit); 5min / 1.5min → invalid duration; U+03BC GREEK SMALL LETTER MU (CE BC) is not a unit — only U+00B5 is in the grammar, even where the underlying duration parser would accept both.
Tests to port from Go: the 47-case token table in encoding/pxf/lexer_duration_test.go (positive, negative, and token-boundary cases), and the property test TestMarshalDurationReadsBack (marshal every Duration.String() branch, read it back to identical seconds/nanos) in encoding/pxf/full_roundtrip_test.go.
Diagnosis is from code reading of the checked-out lexer, not a runtime probe; the line refs above are the two decisions to look at.
Cross-port tracker: filed against every port from the trendvidia/protowire-go#75 session — protowire-rust, protowire-java (Kotlin wraps it), protowire-typescript, protowire-cpp (Python wraps it), protowire-csharp, protowire-dart, protowire-swift.
What
The PXF lexer cannot read fractional or
µsduration literals —1.5ms,1.234567ms,1.5h,2µs,312.5µs,1h30m0.5s— even though draft-trendvidia-protowire-01 §3.3 admits all of them:and §3.10 lists
1.5hand2µsas examples. Two defects, both inherited from the Go reference lexer (fixed there in trendvidia/protowire-go#76 for trendvidia/protowire-go#75):lexNumberchecks for./e/E(→ float) before it checks for a time unit, so1.5mstokenises as FLOAT1.5followed by IDENTms, and the decoder then reports the message-typed Duration field as receiving a scalar (expected '{'or this port's equivalent).a-z, so the two-byte UTF-8µ(U+00B5,C2 B5) never joins the token and2µsbecomes INT2plus a stray byte.Why it matters
These are exactly the forms the Go, Rust, C++ and TypeScript encoders write for a
google.protobuf.Durationthat is not a whole multiple of its largest unit — i.e. every measured latency — because they all mirrortime.Duration.String()(1.234567ms,312.5µs,1h30m0.5s,-2.5s). A document produced by any of those ports with such a value cannot be read by this one.Where
src/pxf/lexer.cc:if (... Peek() == '.' || Peek() == 'e' || Peek() == 'E') return LexFloat(...)runs beforeIsDurationUnit(Peek()).LexDuration(~L560) scansIsDigit || IsLowerAlphaonly;detail::ParseDuration(src/detail/duration.cc) already understandsC2 B5and fractions, so validation is not the blocker, tokenisation is.Encoder:
src/detail/duration.cc(~L164–177) writes%lldµsand%lld.%sµs, so this port cannot read its own output for measured values — same self-inflicted shape as Go.protowire-python wraps this library (
_protowirevia nanobind,protowire_pxftarget) and inherits both the bug and the fix.Fix, as landed in Go (trendvidia/protowire-go#76)
.followed by ≥1 digit) first; then, if a unit start follows (ASCIIh m s n u, or the byte pairC2 B5), take the duration branch; otherwise fall through to float (fraction consumed, or exponent, or bare trailing.), else INT..when followed by a digit, and theC2 B5pair; keep validating the whole literal with the port's Go-duration parser afterwards.1.5→ FLOAT;1.5x→ FLOAT + IDENTx;1.ms→ FLOAT1.+ IDENT (no digit after., so not a duration-segment);1.5e3ms→ FLOAT + IDENT (an exponent is not a unit);5min/1.5min→ invalid duration; U+03BC GREEK SMALL LETTER MU (CE BC) is not a unit — only U+00B5 is in the grammar, even where the underlying duration parser would accept both.Tests to port from Go: the 47-case token table in
encoding/pxf/lexer_duration_test.go(positive, negative, and token-boundary cases), and the property testTestMarshalDurationReadsBack(marshal everyDuration.String()branch, read it back to identical seconds/nanos) inencoding/pxf/full_roundtrip_test.go.Diagnosis is from code reading of the checked-out lexer, not a runtime probe; the line refs above are the two decisions to look at.
Cross-port tracker: filed against every port from the trendvidia/protowire-go#75 session — protowire-rust, protowire-java (Kotlin wraps it), protowire-typescript, protowire-cpp (Python wraps it), protowire-csharp, protowire-dart, protowire-swift.