Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .agents/skills/polyxml-codegen-workflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,34 @@ description: >-

# PolyXML Polyglot Codegen Development & Verification Playbook

## XSD attribute values

Use separate `CARGO_TARGET_DIR` paths for revision-specific CLI/native-binding
verification. A shared debug target can leave another checkout's same-named
top-level binary or cdylib in place even when Cargo reports a fresh build. Probe
a discriminating fixture before claiming a baseline comparison, record the
executable/binding hashes, and retain rejected preflights separately. The runtime
comparison runners already isolate baseline and candidate consumer targets.

Normalize schema attributes with quick-xml's `normalized_value` before placing
them in the IR. This resolves built-in/numeric references once and normalizes
literal XML attribute whitespace while preserving referenced whitespace. Use the
same decoded values for namespace declarations, QName validation, UPA, facets,
enumerations and default/fixed constraints. Propagate malformed attributes and
reference errors; do not turn them into absent values. Validate attributes even
inside otherwise skipped annotations. Regression fixtures are in
`research/fixtures/schema_attribute_entities.xsd` and
`tests/test_schema_attribute_entities.rs` in the core crate.

Decoded enum values need target-language string literal escaping at every output
site: metadata attributes, constants, conversion tables and parse matches. Rust
debug string formatting is suitable for Rust, but its `\u{...}` escapes are not
portable to other targets. The shared JSON-compatible literal helper also
escapes U+0085/U+2028/U+2029 because C# treats them as source line terminators;
C++ uses fixed-width octal escapes for ASCII controls. Execute
`test_enum_literal_codegen` with all seven toolchains to verify exact runtime
values and Rust Serde round trips, not just whether generated files exist.

## C# runtime availability for smoke checks

The C# execution fixtures target `net8.0`. An installed .NET 10 SDK/runtime
Expand Down
21 changes: 16 additions & 5 deletions crates/polyxml-core/src/codegen/cpp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,14 @@ endif()
} else {
""
};
writeln!(out, " {:?}, T::{}{}", v.value, var_name, comma).unwrap();
writeln!(
out,
" {}, T::{}{}",
super::cpp_string_literal(&v.value),
var_name,
comma
)
.unwrap();
}
writeln!(out, " );").unwrap();
}
Expand Down Expand Up @@ -790,8 +797,10 @@ inline int polyxml_integer_compare(std::string_view a,std::string_view b) noexce
for (variant, var_name) in enum_def.variants.iter().zip(&variant_names) {
writeln!(
out,
" case {}::{}: return \"{}\";",
enum_name, var_name, variant.value
" case {}::{}: return {};",
enum_name,
var_name,
super::cpp_string_literal(&variant.value)
)
.unwrap();
}
Expand All @@ -818,8 +827,10 @@ inline int polyxml_integer_compare(std::string_view a,std::string_view b) noexce
for (variant, var_name) in enum_def.variants.iter().zip(&variant_names) {
writeln!(
out,
" if (s == \"{}\") return {}::{};",
variant.value, enum_name, var_name
" if (s == {}) return {}::{};",
super::cpp_string_literal(&variant.value),
enum_name,
var_name
)
.unwrap();
}
Expand Down
15 changes: 12 additions & 3 deletions crates/polyxml-core/src/codegen/csharp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,13 @@ impl CSharpCodegen {
self.emit_docstring(out, doc, &format!("{} ", indent));
}
if self.options.emit_xml_attributes {
writeln!(out, "{} [XmlEnum(\"{}\")]", indent, variant.value).unwrap();
writeln!(
out,
"{} [XmlEnum({})]",
indent,
super::string_literal(&variant.value)
)
.unwrap();
}
writeln!(out, "{} {},", indent, variant_name).unwrap();
}
Expand Down Expand Up @@ -659,8 +665,11 @@ impl CSharpCodegen {
for (variant, variant_name) in e.variants.iter().zip(&variant_names) {
writeln!(
out,
"{} {}.{} => \"{}\",",
indent, enum_name, variant_name, variant.value
"{} {}.{} => {},",
indent,
enum_name,
variant_name,
super::string_literal(&variant.value)
)
.unwrap();
}
Expand Down
10 changes: 6 additions & 4 deletions crates/polyxml-core/src/codegen/go/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ func (v {type_name}) MarshalText() ([]byte,error) {{ tokens:=make([]string,0,len
let variants = enumeration
.variants
.iter()
.map(|v| format!("{:?}", v.value))
.map(|v| super::string_literal(&v.value))
.collect::<Vec<_>>()
.join(",");
writeln!(out,"switch string(value) {{case {variants}: default:return fmt.Errorf(\"invalid enum list item\")}}").unwrap();
Expand Down Expand Up @@ -670,8 +670,10 @@ func (v {type_name}) MarshalText() ([]byte,error) {{ tokens:=make([]string,0,len
}
writeln!(
out,
" {} {} = \"{}\"",
const_name, enum_name, variant.value
" {} {} = {}",
const_name,
enum_name,
super::string_literal(&variant.value)
)
.unwrap();
}
Expand Down Expand Up @@ -925,7 +927,7 @@ func (v {type_name}) MarshalText() ([]byte,error) {{ tokens:=make([]string,0,len
let comparisons = def
.variants
.iter()
.map(|v| format!("value == {:?}", v.value))
.map(|v| format!("value == {}", super::string_literal(&v.value)))
.collect::<Vec<_>>()
.join(" || ");
let _ = writeln!(
Expand Down
7 changes: 5 additions & 2 deletions crates/polyxml-core/src/codegen/java/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,8 +597,11 @@ impl JavaCodegen {
let semi_or_comma = if i + 1 == e.variants.len() { ";" } else { "," };
let _ = writeln!(
out,
"{} {}({:?}){}",
indent, const_name, v.value, semi_or_comma
"{} {}({}){}",
indent,
const_name,
super::string_literal(&v.value),
semi_or_comma
);
}

Expand Down
34 changes: 34 additions & 0 deletions crates/polyxml-core/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,40 @@ pub use python::{
pub use rust::{RustCodegen, RustOptions};
pub use typescript::{TypeScriptBackend, TypeScriptCodegen, TypeScriptOptions};

/// JSON string escapes are also valid in Python, Go, Java, C# and TypeScript
/// string literals. Rust and C++ need their own escapes for control characters.
pub(crate) fn string_literal(value: &str) -> String {
serde_json::to_string(value)
.expect("serializing a string cannot fail")
// C# treats these Unicode characters as source line terminators even
// inside quoted strings; JSON permits their literal forms.
.replace('\u{85}', "\\u0085")
.replace('\u{2028}', "\\u2028")
.replace('\u{2029}', "\\u2029")
}

pub(crate) fn cpp_string_literal(value: &str) -> String {
use std::fmt::Write;
let mut literal = String::from("\"");
for character in value.chars() {
match character {
'\\' => literal.push_str("\\\\"),
'"' => literal.push_str("\\\""),
'\n' => literal.push_str("\\n"),
'\r' => literal.push_str("\\r"),
'\t' => literal.push_str("\\t"),
c if c.is_ascii_control() => {
// Three octal digits prevent a following digit from extending
// the escape; universal character names cannot encode these.
write!(literal, "\\{:03o}", c as u32).unwrap();
}
c => literal.push(c),
}
}
literal.push('"');
literal
}

/// Prefix every physical XSD documentation line so embedded newlines cannot
/// escape a generated source comment.
pub(crate) fn write_documentation_lines(out: &mut String, prefix: &str, doc: &str) {
Expand Down
4 changes: 2 additions & 2 deletions crates/polyxml-core/src/codegen/python/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,9 +521,9 @@ impl PythonCodegen {

let _ = writeln!(
out,
" {} = \"{}\"",
" {} = {}",
var_id,
variant.value.replace('"', "\\\"")
super::string_literal(&variant.value)
);
}
}
Expand Down
8 changes: 4 additions & 4 deletions crates/polyxml-core/src/codegen/rust/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ impl RustCodegen {
write_documentation_lines(out, " /// ", doc);
}
if self.options.derive_serde {
let _ = writeln!(out, " #[serde(rename = \"{}\")]", variant.value);
let _ = writeln!(out, " #[serde(rename = {:?})]", variant.value);
}
if self.options.derive_default && idx == 0 {
let _ = writeln!(out, " #[default]");
Expand All @@ -740,7 +740,7 @@ impl RustCodegen {
out.push_str(" pub fn as_str(&self) -> &'static str {\n");
out.push_str(" match self {\n");
for (var_id, val) in &variant_map {
let _ = writeln!(out, " Self::{} => \"{}\",", var_id, val);
let _ = writeln!(out, " Self::{} => {:?},", var_id, val);
}
if variant_map.is_empty() {
out.push_str(" _ => \"\",\n");
Expand All @@ -755,7 +755,7 @@ impl RustCodegen {
out.push_str(" fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {\n");
out.push_str(" match s {\n");
for (var_id, val) in &variant_map {
let _ = writeln!(out, " \"{}\" => Ok(Self::{}),", val, var_id);
let _ = writeln!(out, " {:?} => Ok(Self::{}),", val, var_id);
}
let _ = writeln!(
out,
Expand Down Expand Up @@ -844,7 +844,7 @@ impl RustCodegen {
write_documentation_lines(out, " /// ", doc);
}
if self.options.derive_serde && !u.is_lexical() {
let _ = writeln!(out, " #[serde(rename = \"{}\")]", branch.xml_name);
let _ = writeln!(out, " #[serde(rename = {:?})]", branch.xml_name);
}
if self.options.emit_polyxml_attrs && !u.is_lexical() {
let _ = writeln!(out, " #[polyxml(element = \"{}\")]", branch.xml_name);
Expand Down
11 changes: 8 additions & 3 deletions crates/polyxml-core/src/codegen/typescript/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,12 @@ impl TypeScriptCodegen {
let mut seen = HashSet::new();
for v in &e.variants {
let variant_key = self.unique_variant_name(&v.name, &mut seen);
let _ = writeln!(out, " {}: {:?},", variant_key, v.value);
let _ = writeln!(
out,
" {}: {},",
variant_key,
super::string_literal(&v.value)
);
}
out.push_str("} as const;\n\n");

Expand All @@ -437,7 +442,7 @@ impl TypeScriptCodegen {
let literals = e
.variants
.iter()
.map(|v| format!("{:?}", v.value))
.map(|v| super::string_literal(&v.value))
.collect::<Vec<_>>()
.join(", ");

Expand All @@ -461,7 +466,7 @@ impl TypeScriptCodegen {
let tb_literals = e
.variants
.iter()
.map(|v| format!("Type.Literal({:?})", v.value))
.map(|v| format!("Type.Literal({})", super::string_literal(&v.value)))
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(
Expand Down
Loading