Skip to content
Merged
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
346 changes: 317 additions & 29 deletions crates/message-format-conformance/src/harness.rs

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions crates/message-format-conformance/src/tr35/number_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,14 @@ fn number_style_percent() {
#[test]
fn stored_decimal_percent_shifts_the_decimal_point() {
for (source, expected) in [
(".local $x = {0.123 :number} {{{$x :percent}}}", "12.3%"),
(".local $x = {-0.123 :number} {{{$x :percent}}}", "-12.3%"),
(
".local $x = {0.123 :number} {{{$x :percent maximumFractionDigits=3}}}",
"12.3%",
),
(
".local $x = {-0.123 :number} {{{$x :percent maximumFractionDigits=3}}}",
"-12.3%",
),
(
".local $x = {1.234 :number} {{{$x :number style=percent}}}",
"123.4%",
Expand Down
16 changes: 13 additions & 3 deletions crates/message-format-conformance/src/tr35/pattern_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,20 +174,30 @@ fn selector_operand_failure_short_circuits_option_resolution() {
assert_eq!(output.value, "CATCHALL");
assert_errors_multiset(
&output.errors,
&[missing_arg("x"), FormatError::BadSelector { source: None }],
&[
missing_arg("x"),
function_error(MessageFunctionError::BadOperand),
FormatError::BadSelector { source: None },
],
);
}

/// A fallback propagated through a local is re-annotated using the current
/// variable's fallback representation, without invoking the function host.
/// variable's fallback representation, while reporting the bad operand.
#[test]
fn local_fallback_reannotation_uses_current_variable_name() {
let output = format_output(
".local $a = {$missing}\n.local $b = {$a :number}\n{{{$b}}}",
&[],
);
assert_eq!(output.value, "{$b}");
assert_errors_multiset(&output.errors, &[missing_arg("missing")]);
assert_errors_multiset(
&output.errors,
&[
missing_arg("missing"),
function_error(MessageFunctionError::BadOperand),
],
);
}

/// Plain local aliases retain their own fallback identity when rendered.
Expand Down
17 changes: 13 additions & 4 deletions crates/message-format-conformance/src/tr35/string_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use super::helpers::*;
use message_format::compiler::CompileOptions;
use message_format::runtime::{FormatError, Value};
use message_format::runtime::{FormatError, MessageFunctionError, Value};

// ---------------------------------------------------------------------------
// TR35 §13 — :string function
Expand Down Expand Up @@ -152,15 +152,21 @@ fn string_numeric_chain_remains_eager_before_selection() {
assert!(is_bad_operand(&output.errors[0]));
}

/// A missing string input remains a recoverable missing-argument error.
/// Reannotating a missing string input reports the numeric operand failure.
#[test]
fn missing_string_input_reannotation_errors() {
let output = format_output(
".input {$x :string} .local $y = {$x :number} {{value={$y}}}",
&[],
);
assert_eq!(output.value, "value={$y}");
assert_errors_multiset(&output.errors, &[FormatError::MissingArg("x".to_string())]);
assert_errors_multiset(
&output.errors,
&[
FormatError::MissingArg("x".to_string()),
function_error(MessageFunctionError::BadOperand),
],
);
}

/// Inlining does not duplicate a non-elidable local string declaration.
Expand All @@ -173,7 +179,10 @@ fn local_string_numeric_chain_reports_missing_input_once() {
assert_eq!(output.value, "value={$n}");
assert_errors_multiset(
&output.errors,
&[FormatError::MissingArg("raw".to_string())],
&[
FormatError::MissingArg("raw".to_string()),
function_error(MessageFunctionError::BadOperand),
],
);
}

Expand Down
4 changes: 3 additions & 1 deletion crates/message-format/src/compiler/compile/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ impl DeclarationUses {
}
self.visit_parts(default, aliases);
}
Part::MarkupOpen { options, .. } | Part::MarkupClose { options, .. } => {
Part::MarkupOpen { options, .. }
| Part::MarkupClose { options, .. }
| Part::MarkupStandalone { options, .. } => {
self.visit_options(options);
}
Part::Text(_) | Part::Literal(_) | Part::Var(_) => {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ pub(super) fn builtin_selector_accepts_variant_key(selector: &SelectorExpr, key:
fn builtin_numeric_selector_mode_for_func(
func: &FunctionSpec,
) -> Option<BuiltinNumericSelectorMode> {
if func.name == "offset" {
if matches!(func.name.as_str(), "offset" | "percent") {
return Some(BuiltinNumericSelectorMode::Plural);
}
if !matches!(func.name.as_str(), "number" | "integer") {
Expand Down Expand Up @@ -451,7 +451,7 @@ struct NumericSelectorLoweringPlan {
fn numeric_selector_lowering_plan(selector: &SelectorExpr) -> Option<NumericSelectorLoweringPlan> {
match selector {
SelectorExpr::Call { operand, func }
if matches!(func.name.as_str(), "number" | "integer") =>
if matches!(func.name.as_str(), "number" | "integer" | "percent") =>
{
match builtin_numeric_selector_mode_for_func(func)? {
BuiltinNumericSelectorMode::Exact => None,
Expand Down
29 changes: 5 additions & 24 deletions crates/message-format/src/compiler/compile/frontend/pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,6 @@ pub(super) fn lower_pattern_node_to_parts(
plan,
None,
)?;
// Self-closing markup: emit open + close in sequence.
if is_self_close_markup(expr)
&& let Part::MarkupOpen { ref name, .. } = part
{
let close_name = name.clone();
parts.push(part);
parts.push(Part::MarkupClose {
name: close_name,
options: Vec::new(),
});
continue;
}
parts.push(part);
}
}
Expand Down Expand Up @@ -206,7 +194,7 @@ fn lower_expression_payload_node_to_part(
if context.default_bidi_isolation {
let mut call = CallExpr {
operand: Operand::Var(var),
func: FunctionSpec::new("string").option_literal("u:dir", "auto"),
func: FunctionSpec::new("string").option_literal("u:dir", "\0inherit"),
fallback: None,
};
resolve_call(&mut call, plan, excluded);
Expand Down Expand Up @@ -270,7 +258,7 @@ fn lower_expression_payload_node_to_part(
name: canonicalize_identifier(markup.identifier),
options,
}),
crate::compiler::syntax::ast::MarkupKind::SelfClose => Ok(Part::MarkupOpen {
crate::compiler::syntax::ast::MarkupKind::SelfClose => Ok(Part::MarkupStandalone {
name: canonicalize_identifier(markup.identifier),
options,
}),
Expand Down Expand Up @@ -304,7 +292,7 @@ fn lower_expression_payload_node_to_part(
if context.default_bidi_isolation {
return Ok(Part::Call(CallExpr {
operand: lower_literal_expression_operand(&literal.value_span, value, source),
func: FunctionSpec::new("string").option_literal("u:dir", "auto"),
func: FunctionSpec::new("string").option_literal("u:dir", "\0inherit"),
fallback: None,
}));
}
Expand Down Expand Up @@ -423,7 +411,8 @@ fn apply_default_bidi_direction(func: &mut FunctionSpec, context: ExpressionLowe
&& func.name == "string"
&& !func.options.iter().any(|option| option.key == "u:dir")
{
func.options.push(FunctionOption::literal("u:dir", "auto"));
func.options
.push(FunctionOption::literal("u:dir", "\0inherit"));
}
}

Expand Down Expand Up @@ -468,14 +457,6 @@ fn classify_operand_literal_kind(
}
}

fn is_self_close_markup(expr: &crate::compiler::syntax::ast::ExpressionNode<'_>) -> bool {
matches!(
&expr.payload,
Some(crate::compiler::syntax::ast::ExpressionPayloadNode::Markup(m))
if m.kind == crate::compiler::syntax::ast::MarkupKind::SelfClose
)
}

fn lower_markup_options(
source: &str,
options: &[crate::compiler::syntax::ast::OptionNode<'_>],
Expand Down
4 changes: 3 additions & 1 deletion crates/message-format/src/compiler/compile/interning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ impl CatalogItems {
}
self.visit_parts(default)?;
}
Part::MarkupOpen { name, options } | Part::MarkupClose { name, options } => {
Part::MarkupOpen { name, options }
| Part::MarkupClose { name, options }
| Part::MarkupStandalone { name, options } => {
self.strings.insert(name.clone());
self.visit_options(options);
}
Expand Down
27 changes: 12 additions & 15 deletions crates/message-format/src/compiler/compile/lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,24 +218,20 @@ fn lower_parts_inner<'a>(
emit_call(call, string_map, func_map, code)?;
code.push(schema::Opcode::OutVal as u8);
}
Part::MarkupOpen { name, options } => {
Part::MarkupOpen { name, options }
| Part::MarkupClose { name, options }
| Part::MarkupStandalone { name, options } => {
emit_markup_options(options, string_map, code)?;
let name_str_id = *string_map
.get(name)
.ok_or(CompileError::internal("missing interned markup name"))?;
code.push(schema::Opcode::MarkupOpen as u8);
code.extend_from_slice(&name_str_id.to_le_bytes());
code.push(
u8::try_from(options.len())
.map_err(|_| CompileError::size_overflow("option count"))?,
);
}
Part::MarkupClose { name, options } => {
emit_markup_options(options, string_map, code)?;
let name_str_id = *string_map
.get(name)
.ok_or(CompileError::internal("missing interned markup name"))?;
code.push(schema::Opcode::MarkupClose as u8);
let opcode = match part {
Part::MarkupOpen { .. } => schema::Opcode::MarkupOpen,
Part::MarkupClose { .. } => schema::Opcode::MarkupClose,
Part::MarkupStandalone { .. } => schema::Opcode::MarkupStandalone,
_ => unreachable!("combined markup arm only matches markup"),
};
code.push(opcode as u8);
code.extend_from_slice(&name_str_id.to_le_bytes());
code.push(
u8::try_from(options.len())
Expand Down Expand Up @@ -297,7 +293,8 @@ fn emit_value_part(
| Part::Text(_)
| Part::Select(_)
| Part::MarkupOpen { .. }
| Part::MarkupClose { .. } => {
| Part::MarkupClose { .. }
| Part::MarkupStandalone { .. } => {
return Err(CompileError::internal("non-scalar declaration expression"));
}
}
Expand Down
8 changes: 7 additions & 1 deletion crates/message-format/src/compiler/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,7 @@ fn collect_builtin_part_errors(parts: &[Part], message: &Message, errors: &mut V
}
Part::MarkupOpen { .. }
| Part::MarkupClose { .. }
| Part::MarkupStandalone { .. }
| Part::Text(_)
| Part::Literal(_)
| Part::Var(_)
Expand Down Expand Up @@ -1038,6 +1039,9 @@ fn collect_builtin_function_errors(
let FunctionOptionValue::Literal(found) = &option.value else {
continue;
};
if func.name == "string" && option.key == "u:dir" && found == "\0inherit" {
continue;
}
if value.iter().any(|candidate| candidate == found) {
continue;
}
Expand Down Expand Up @@ -1168,7 +1172,9 @@ fn collect_manifest_part_errors(
| Part::Var(_)
| Part::Local(_)
| Part::CheckSelector(_) => {}
Part::MarkupOpen { name, options } | Part::MarkupClose { name, options } => {
Part::MarkupOpen { name, options }
| Part::MarkupClose { name, options }
| Part::MarkupStandalone { name, options } => {
collect_markup_manifest_errors_into(name, options, manifest, message, errors);
}
}
Expand Down
Loading