Skip to content

Commit d8c6037

Browse files
committed
Handle context-dependent number literals in OpConstant
The assembler now checks the result type when encoding OpConstant operands. Integer text like "42" for a float32 type is interpreted as the float value 42.0 and stored as its IEEE 754 bit pattern, matching the C++ spirv-as behavior for LiteralContextDependentNumber. Float text like "42.5" is also accepted for float types by falling back to OperandValue::Word in the parser when integer parsing fails.
1 parent 968069d commit d8c6037

File tree

2 files changed

+350
-9
lines changed

2 files changed

+350
-9
lines changed

rust/spirv-tools-core/src/assembly/assembler.rs

Lines changed: 340 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,8 +1134,87 @@ impl<'a> AssemblyTranslator<'a> {
11341134
return;
11351135
}
11361136
};
1137-
let literal = match literal_operand.value() {
1138-
OperandValue::Literal(value) => value,
1137+
1138+
let (type_id, result_id) = self
1139+
.module_builder
1140+
.bind_typed_result(result_type, result_id);
1141+
1142+
// Look up the result type to determine how to encode the literal.
1143+
// The operand is a LiteralContextDependentNumber: its encoding depends
1144+
// on the result type, matching the C++ spirv-as behavior.
1145+
let float_width = self.lookup_float_type_width(type_id);
1146+
1147+
let operands = match literal_operand.value() {
1148+
OperandValue::Literal(literal) => {
1149+
// Integer text (e.g. "42", "-1"). For float types, convert the
1150+
// integer value to float then encode as IEEE 754 bit pattern.
1151+
match float_width {
1152+
Some(32) => {
1153+
let float_val = match literal {
1154+
LiteralNumber::Unsigned(v) => *v as f32,
1155+
LiteralNumber::Signed(v) => *v as f32,
1156+
};
1157+
vec![dr::Operand::LiteralBit32(float_val.to_bits())]
1158+
}
1159+
Some(64) => {
1160+
let float_val = match literal {
1161+
LiteralNumber::Unsigned(v) => *v as f64,
1162+
LiteralNumber::Signed(v) => *v as f64,
1163+
};
1164+
vec![dr::Operand::LiteralBit64(float_val.to_bits())]
1165+
}
1166+
Some(_) => {
1167+
// 16-bit or other widths: encode as raw bits in a 32-bit word
1168+
vec![dr::Operand::LiteralBit32(literal_to_u32(literal))]
1169+
}
1170+
None => {
1171+
// Integer type: encode as raw integer bits.
1172+
self.module_builder
1173+
.note_integer_constant(result_id, literal_to_u64(literal));
1174+
vec![encode_literal_operand(literal)]
1175+
}
1176+
}
1177+
}
1178+
OperandValue::Word(word) => {
1179+
// Non-integer text (e.g. "42.5", "0x1.8p+1"). Must be a float type.
1180+
let text = word.as_str();
1181+
if float_width.is_none() {
1182+
self.module_builder.emit_error(
1183+
literal_operand.span().start(),
1184+
"integer type requires an integer literal",
1185+
);
1186+
return;
1187+
}
1188+
match float_width {
1189+
Some(32) => match text.parse::<f32>() {
1190+
Ok(val) => vec![dr::Operand::LiteralBit32(val.to_bits())],
1191+
Err(_) => {
1192+
self.module_builder.emit_error(
1193+
literal_operand.span().start(),
1194+
"invalid 32-bit float literal",
1195+
);
1196+
return;
1197+
}
1198+
},
1199+
Some(64) => match text.parse::<f64>() {
1200+
Ok(val) => vec![dr::Operand::LiteralBit64(val.to_bits())],
1201+
Err(_) => {
1202+
self.module_builder.emit_error(
1203+
literal_operand.span().start(),
1204+
"invalid 64-bit float literal",
1205+
);
1206+
return;
1207+
}
1208+
},
1209+
_ => {
1210+
self.module_builder.emit_error(
1211+
literal_operand.span().start(),
1212+
"unsupported float width for literal",
1213+
);
1214+
return;
1215+
}
1216+
}
1217+
}
11391218
_ => {
11401219
self.module_builder.emit_error(
11411220
literal_operand.span().start(),
@@ -1145,21 +1224,33 @@ impl<'a> AssemblyTranslator<'a> {
11451224
}
11461225
};
11471226

1148-
let (type_id, result_id) = self
1149-
.module_builder
1150-
.bind_typed_result(result_type, result_id);
11511227
let inst = dr::Instruction::new(
11521228
spirv::Op::Constant,
11531229
Some(type_id),
11541230
Some(result_id),
1155-
vec![dr::Operand::LiteralBit32(literal_to_u32(literal))],
1231+
operands,
11561232
);
1157-
self.module_builder
1158-
.note_integer_constant(result_id, literal_to_u64(literal));
11591233
self.builder.module_mut().types_global_values.push(inst);
11601234
self.record_from_module(|module| module.types_global_values.last().cloned());
11611235
}
11621236

1237+
/// Look up whether `type_id` refers to an `OpTypeFloat` instruction
1238+
/// and return its bit width if so.
1239+
fn lookup_float_type_width(&self, type_id: u32) -> Option<u32> {
1240+
self.builder
1241+
.module_ref()
1242+
.types_global_values
1243+
.iter()
1244+
.find(|inst| {
1245+
inst.class.opcode == spirv::Op::TypeFloat && inst.result_id == Some(type_id)
1246+
})
1247+
.and_then(|inst| inst.operands.first())
1248+
.and_then(|op| match op {
1249+
dr::Operand::LiteralBit32(w) => Some(*w),
1250+
_ => None,
1251+
})
1252+
}
1253+
11631254
fn translate_type_function(&mut self, instruction: &ParsedInstruction<'a>) {
11641255
let opcode_pos = instruction.opcode_position();
11651256
let Some(result_id) = instruction.result_id() else {
@@ -5913,4 +6004,245 @@ OpFunctionEnd"#;
59136004
assert!(convert.result_id.is_some());
59146005
assert_eq!(convert.operands.len(), 1);
59156006
}
6007+
6008+
// ---------------------------------------------------------------
6009+
// Context-dependent number literal tests (OpConstant / OpSpecConstant)
6010+
// Matching C++ spirv-as: integer text for float types is parsed as
6011+
// the float value, not raw bits.
6012+
// ---------------------------------------------------------------
6013+
6014+
/// Helper: assemble text, find the first OpConstant, return its operand.
6015+
fn assemble_and_get_constant_operand(lines: &[&str]) -> dr::Operand {
6016+
let parsed: Vec<_> = lines
6017+
.iter()
6018+
.map(|line| parse_instruction(line).expect("parse"))
6019+
.collect();
6020+
let refs: Vec<_> = parsed.iter().collect();
6021+
let module = assemble_instructions(&refs).expect("assemble");
6022+
module
6023+
.types_global_values
6024+
.iter()
6025+
.find(|inst| inst.class.opcode == spirv::Op::Constant)
6026+
.expect("OpConstant not found")
6027+
.operands
6028+
.first()
6029+
.expect("OpConstant missing operand")
6030+
.clone()
6031+
}
6032+
6033+
#[test]
6034+
fn constant_integer_literal_for_float32_encodes_as_float_value() {
6035+
// "42" with float32 type should encode as float 42.0, not raw bits 0x2A.
6036+
let operand = assemble_and_get_constant_operand(&[
6037+
"%float = OpTypeFloat 32",
6038+
"%c = OpConstant %float 42",
6039+
]);
6040+
assert_eq!(operand, dr::Operand::LiteralBit32(42.0_f32.to_bits()));
6041+
}
6042+
6043+
#[test]
6044+
fn constant_zero_for_float32_encodes_correctly() {
6045+
let operand = assemble_and_get_constant_operand(&[
6046+
"%float = OpTypeFloat 32",
6047+
"%c = OpConstant %float 0",
6048+
]);
6049+
assert_eq!(operand, dr::Operand::LiteralBit32(0.0_f32.to_bits()));
6050+
}
6051+
6052+
#[test]
6053+
fn constant_one_for_float32_encodes_correctly() {
6054+
let operand = assemble_and_get_constant_operand(&[
6055+
"%float = OpTypeFloat 32",
6056+
"%c = OpConstant %float 1",
6057+
]);
6058+
assert_eq!(operand, dr::Operand::LiteralBit32(1.0_f32.to_bits()));
6059+
}
6060+
6061+
#[test]
6062+
fn constant_negative_integer_for_float32_encodes_as_negative_float() {
6063+
// "-1" with float32 type should encode as -1.0f (0xBF800000).
6064+
let operand = assemble_and_get_constant_operand(&[
6065+
"%float = OpTypeFloat 32",
6066+
"%c = OpConstant %float -1",
6067+
]);
6068+
assert_eq!(operand, dr::Operand::LiteralBit32((-1.0_f32).to_bits()));
6069+
}
6070+
6071+
#[test]
6072+
fn constant_large_integer_for_float32_encodes_as_float() {
6073+
let operand = assemble_and_get_constant_operand(&[
6074+
"%float = OpTypeFloat 32",
6075+
"%c = OpConstant %float 1000000",
6076+
]);
6077+
assert_eq!(operand, dr::Operand::LiteralBit32(1_000_000.0_f32.to_bits()));
6078+
}
6079+
6080+
#[test]
6081+
fn constant_integer_literal_for_float64_encodes_as_double_value() {
6082+
// "42" with float64 type should encode as double 42.0.
6083+
let operand = assemble_and_get_constant_operand(&[
6084+
"%double = OpTypeFloat 64",
6085+
"%c = OpConstant %double 42",
6086+
]);
6087+
assert_eq!(operand, dr::Operand::LiteralBit64(42.0_f64.to_bits()));
6088+
}
6089+
6090+
#[test]
6091+
fn constant_negative_integer_for_float64_encodes_as_negative_double() {
6092+
let operand = assemble_and_get_constant_operand(&[
6093+
"%double = OpTypeFloat 64",
6094+
"%c = OpConstant %double -1",
6095+
]);
6096+
assert_eq!(operand, dr::Operand::LiteralBit64((-1.0_f64).to_bits()));
6097+
}
6098+
6099+
#[test]
6100+
fn constant_float_text_for_float32_encodes_correctly() {
6101+
// "42.5" is float text, parsed via OperandValue::Word path.
6102+
let operand = assemble_and_get_constant_operand(&[
6103+
"%float = OpTypeFloat 32",
6104+
"%c = OpConstant %float 42.5",
6105+
]);
6106+
assert_eq!(operand, dr::Operand::LiteralBit32(42.5_f32.to_bits()));
6107+
}
6108+
6109+
#[test]
6110+
fn constant_negative_float_text_for_float32_encodes_correctly() {
6111+
let operand = assemble_and_get_constant_operand(&[
6112+
"%float = OpTypeFloat 32",
6113+
"%c = OpConstant %float -3.14",
6114+
]);
6115+
assert_eq!(operand, dr::Operand::LiteralBit32((-3.14_f32).to_bits()));
6116+
}
6117+
6118+
#[test]
6119+
fn constant_float_text_for_float64_encodes_correctly() {
6120+
let operand = assemble_and_get_constant_operand(&[
6121+
"%double = OpTypeFloat 64",
6122+
"%c = OpConstant %double 42.5",
6123+
]);
6124+
assert_eq!(operand, dr::Operand::LiteralBit64(42.5_f64.to_bits()));
6125+
}
6126+
6127+
#[test]
6128+
fn constant_integer_for_uint32_encodes_as_raw_bits() {
6129+
// Integer types should still encode as raw integer bits.
6130+
let operand = assemble_and_get_constant_operand(&[
6131+
"%uint = OpTypeInt 32 0",
6132+
"%c = OpConstant %uint 42",
6133+
]);
6134+
assert_eq!(operand, dr::Operand::LiteralBit32(42));
6135+
}
6136+
6137+
#[test]
6138+
fn constant_integer_for_sint32_encodes_as_raw_bits() {
6139+
let operand = assemble_and_get_constant_operand(&[
6140+
"%int = OpTypeInt 32 1",
6141+
"%c = OpConstant %int 42",
6142+
]);
6143+
assert_eq!(operand, dr::Operand::LiteralBit32(42));
6144+
}
6145+
6146+
#[test]
6147+
fn constant_negative_for_sint32_encodes_twos_complement() {
6148+
let operand = assemble_and_get_constant_operand(&[
6149+
"%int = OpTypeInt 32 1",
6150+
"%c = OpConstant %int -1",
6151+
]);
6152+
// -1 as i32 in two's complement is 0xFFFFFFFF
6153+
assert_eq!(operand, dr::Operand::LiteralBit32((-1_i32) as u32));
6154+
}
6155+
6156+
#[test]
6157+
fn constant_integer_for_uint64_encodes_value() {
6158+
// Small values that fit in 32 bits are stored as LiteralBit32 by
6159+
// encode_literal_operand. This is a pre-existing behavior; the binary
6160+
// serializer handles type-width encoding.
6161+
let operand = assemble_and_get_constant_operand(&[
6162+
"%ulong = OpTypeInt 64 0",
6163+
"%c = OpConstant %ulong 42",
6164+
]);
6165+
assert_eq!(operand, dr::Operand::LiteralBit32(42));
6166+
}
6167+
6168+
#[test]
6169+
fn constant_large_integer_for_uint64_encodes_as_64bit() {
6170+
// Values that don't fit in 32 bits should use LiteralBit64.
6171+
let operand = assemble_and_get_constant_operand(&[
6172+
"%ulong = OpTypeInt 64 0",
6173+
"%c = OpConstant %ulong 4294967296",
6174+
]);
6175+
assert_eq!(operand, dr::Operand::LiteralBit64(4_294_967_296));
6176+
}
6177+
6178+
#[test]
6179+
fn constant_float_round_trips_through_assemble_disassemble() {
6180+
// Full round-trip: assemble "OpConstant %float 42" then disassemble
6181+
// and verify the output shows 42 (the float value), not 5.88545e-44.
6182+
let text = [
6183+
"OpCapability Shader",
6184+
"OpMemoryModel Logical GLSL450",
6185+
"%float = OpTypeFloat 32",
6186+
"%c = OpConstant %float 42",
6187+
]
6188+
.join("\n");
6189+
let disassembled = round_trip_with_options(
6190+
&text,
6191+
TextToBinaryOptions::NONE,
6192+
BinaryToTextOptions::NO_HEADER,
6193+
);
6194+
assert!(
6195+
disassembled.contains("OpConstant") && disassembled.contains(" 42"),
6196+
"Expected disassembly to contain 'OpConstant ... 42', got: {disassembled}"
6197+
);
6198+
// Must NOT contain the subnormal float that 0x2A bit pattern represents
6199+
assert!(
6200+
!disassembled.contains("5.88545"),
6201+
"OpConstant should not show raw bits interpretation: {disassembled}"
6202+
);
6203+
}
6204+
6205+
#[test]
6206+
fn constant_float_text_round_trips_through_assemble_disassemble() {
6207+
let text = [
6208+
"OpCapability Shader",
6209+
"OpMemoryModel Logical GLSL450",
6210+
"%float = OpTypeFloat 32",
6211+
"%c = OpConstant %float 42.5",
6212+
]
6213+
.join("\n");
6214+
let disassembled = round_trip_with_options(
6215+
&text,
6216+
TextToBinaryOptions::NONE,
6217+
BinaryToTextOptions::NO_HEADER,
6218+
);
6219+
assert!(
6220+
disassembled.contains("42.5"),
6221+
"Expected disassembly to contain '42.5', got: {disassembled}"
6222+
);
6223+
}
6224+
6225+
#[test]
6226+
fn constant_does_not_note_integer_constant_for_float_type() {
6227+
// When the type is float, note_integer_constant should NOT be called.
6228+
// Verify by checking that a subsequent array-length lookup doesn't
6229+
// confuse float bits with integer values.
6230+
let type_inst = parse_instruction("%float = OpTypeFloat 32").unwrap();
6231+
let const_inst = parse_instruction("%c = OpConstant %float 42").unwrap();
6232+
let mut translator = AssemblyTranslator::new();
6233+
translator.translate(&type_inst);
6234+
translator.translate(&const_inst);
6235+
let (module, diagnostics) = translator.finish();
6236+
assert!(diagnostics.is_empty());
6237+
let constant = module
6238+
.types_global_values
6239+
.iter()
6240+
.find(|inst| inst.class.opcode == spirv::Op::Constant)
6241+
.expect("constant");
6242+
// The operand should be 42.0f bits, not integer 42
6243+
assert_eq!(
6244+
constant.operands.first().unwrap(),
6245+
&dr::Operand::LiteralBit32(42.0_f32.to_bits())
6246+
);
6247+
}
59166248
}

rust/spirv-tools-core/src/assembly/parser.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,9 +400,18 @@ impl<'a> Parser<'a> {
400400
OperandValue::Id(IdRef::new(id, span))
401401
}
402402
}
403-
OperandKind::LiteralInteger | OperandKind::LiteralContextDependentNumber => {
403+
OperandKind::LiteralInteger => {
404404
OperandValue::Literal(parse_integer(word, span)?)
405405
}
406+
OperandKind::LiteralContextDependentNumber => {
407+
// Context-dependent numbers may be integer or float text
408+
// depending on the result type (e.g. OpConstant %float 42.5).
409+
// Fall back to Word so the assembler can do type-aware parsing.
410+
match parse_integer(word, span) {
411+
Ok(literal) => OperandValue::Literal(literal),
412+
Err(_) => OperandValue::Word(word),
413+
}
414+
}
406415
OperandKind::LiteralExtInstInteger => match parse_integer(word, span) {
407416
Ok(literal) => OperandValue::Literal(literal),
408417
Err(_) => OperandValue::Word(word),

0 commit comments

Comments
 (0)