Skip to content

Commit d4f645a

Browse files
committed
core: add fixed String<N> primitives
Add fixed-size String<N> helpers for byte round-trips, effective byte length, concatenation, and equality. String::as_bytes() is implemented with masking + shifting so it works for runtime values (avoids CTFE-only intrinsic::__as_bytes). Includes a runtime fixture that exercises as_bytes on a String parameter. Extends semantic lowering/CTFE and MIR runtime type info so fixed strings behave consistently (padded literal lowering, word casts, CTFE conversions/equality), plus a semantic CTFE regression test.
1 parent 6204a91 commit d4f645a

13 files changed

Lines changed: 783 additions & 10 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use core::abi::{ByteInput, MemoryInput, encode_alloc}
2+
use core::concat
3+
use std::abi::Sol
4+
5+
fn fifth_byte(_ s: String<8>) -> u8 {
6+
let bytes: [u8; 8] = s.as_bytes()
7+
bytes[4]
8+
}
9+
10+
#[test]
11+
fn test_string_len_runtime() {
12+
let s: String<8> = "COOL"
13+
assert(s.len() == 4)
14+
}
15+
16+
#[test]
17+
fn test_string_as_bytes_runtime() {
18+
let s: String<8> = "COOL"
19+
let bytes: [u8; 8] = s.as_bytes()
20+
assert(bytes[0] == 0)
21+
assert(bytes[3] == 0)
22+
assert(bytes[4] == 67)
23+
assert(bytes[7] == 76)
24+
}
25+
26+
#[test]
27+
fn test_string_as_bytes_param_runtime() {
28+
let encoded: (u256, u256) = encode_alloc<Sol, String<8>>("COOL")
29+
let input = MemoryInput {
30+
base: encoded.0,
31+
len: encoded.1,
32+
}
33+
34+
let mut padded: [u8; 8] = [0; 8]
35+
padded[4] = input.byte_at(32)
36+
padded[5] = input.byte_at(33)
37+
padded[6] = input.byte_at(34)
38+
padded[7] = input.byte_at(35)
39+
40+
let s: String<8> = String::from_bytes(padded)
41+
assert(fifth_byte(s) == 67)
42+
}
43+
44+
#[test]
45+
fn test_string_roundtrip_bytes_runtime() {
46+
let bytes: [u8; 8] = [0, 0, 0, 0, 67, 79, 79, 76]
47+
let s: String<8> = String::from_bytes(bytes)
48+
assert(s == "COOL")
49+
}
50+
51+
#[test]
52+
fn test_string_from_bytes_expression_runtime() {
53+
let s: String<8> = String::from_bytes([0, 0, 0, 0, 67, 79, 79, 76])
54+
assert(s == "COOL")
55+
}
56+
57+
#[test]
58+
fn test_string_concat_runtime() {
59+
let a: String<8> = "COOL"
60+
let b: String<8> = "COIN"
61+
let c: String<8> = concat(a, b)
62+
assert(c == "COOLCOIN")
63+
}
64+
65+
#[test]
66+
fn test_string_concat_expression_args_runtime() {
67+
let left: String<2> = "CO"
68+
let right: String<2> = "OL"
69+
let c: String<4> = concat((left), (right))
70+
assert(c == "COOL")
71+
}
72+
73+
#[test]
74+
fn test_string_concat_uses_effective_len_runtime() {
75+
let a: String<4> = "C"
76+
let b: String<4> = "O"
77+
let c: String<4> = concat(a, b)
78+
assert(c == "CO")
79+
}
80+
81+
#[test]
82+
fn test_string_concat_overflow_truncates() {
83+
let a: String<4> = "ABCD"
84+
let b: String<4> = "EFGH"
85+
let c: String<6> = concat(a, b)
86+
assert(c == "ABCDEF")
87+
}
88+
89+
#[test]
90+
fn test_string_abi_encoding_ignores_hidden_high_bytes() {
91+
let s: String<4> = 0x01000000434f4f4c as String<4>
92+
let encoded: (u256, u256) = encode_alloc<Sol, String<4>>(s)
93+
let input = MemoryInput {
94+
base: encoded.0,
95+
len: encoded.1,
96+
}
97+
98+
assert(input.word_at(0) == 4)
99+
assert(input.byte_at(32) == 67)
100+
assert(input.byte_at(35) == 76)
101+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
source: crates/fe/tests/cli_output.rs
3+
expression: output
4+
input_file: tests/fixtures/fe_test_runner/fixed_string_primitives.fe
5+
---
6+
=== STDOUT ===
7+
PASS [<time>] test_string_len_runtime
8+
PASS [<time>] test_string_as_bytes_runtime
9+
PASS [<time>] test_string_as_bytes_param_runtime
10+
PASS [<time>] test_string_roundtrip_bytes_runtime
11+
PASS [<time>] test_string_from_bytes_expression_runtime
12+
PASS [<time>] test_string_concat_runtime
13+
PASS [<time>] test_string_concat_expression_args_runtime
14+
PASS [<time>] test_string_concat_uses_effective_len_runtime
15+
PASS [<time>] test_string_concat_overflow_truncates
16+
PASS [<time>] test_string_abi_encoding_ignores_hidden_high_bytes
17+
18+
test result: ok. 10 passed; 0 failed
19+
20+
=== EXIT CODE: 0 ===

crates/hir/src/analysis/semantic/consts.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::analysis::{
88
semantic::{SemanticInstance, instantiate_with_generic_args},
99
ty::{
1010
const_ty::{ConstTyData, EvaluatedConstTy, evaluate_type_level_int_const_expr},
11-
ty_def::{PrimTy, TyBase, TyData, TyId, prim_int_bits},
11+
ty_def::{PrimTy, TyBase, TyData, TyId, TyVarSort, prim_int_bits},
1212
},
1313
};
1414

@@ -143,6 +143,31 @@ pub fn sem_const_eq<'db>(
143143
lhs: SemConstId<'db>,
144144
rhs: SemConstId<'db>,
145145
) -> bool {
146+
fn fixed_string_bytes_eq(a: &[u8], b: &[u8]) -> bool {
147+
fn trim_leading_zeros(bytes: &[u8]) -> &[u8] {
148+
let mut idx = 0usize;
149+
while idx < bytes.len() && bytes[idx] == 0 {
150+
idx += 1;
151+
}
152+
&bytes[idx..]
153+
}
154+
155+
trim_leading_zeros(a) == trim_leading_zeros(b)
156+
}
157+
158+
fn is_string_like<'db>(db: &'db dyn HirAnalysisDb, ty: TyId<'db>) -> bool {
159+
let ty = ty.as_capability(db).map(|(_, inner)| inner).unwrap_or(ty);
160+
if ty.is_string(db) {
161+
return true;
162+
}
163+
164+
let base = ty.base_ty(db);
165+
matches!(
166+
base.data(db),
167+
TyData::TyVar(var) if matches!(var.sort, TyVarSort::String { .. })
168+
)
169+
}
170+
146171
if lhs == rhs {
147172
return true;
148173
}
@@ -157,7 +182,19 @@ pub fn sem_const_eq<'db>(
157182
ty: rhs_ty,
158183
value: rhs_value,
159184
},
160-
) => lhs_ty == rhs_ty && lhs_value == rhs_value,
185+
) => {
186+
if is_string_like(db, lhs_ty)
187+
&& is_string_like(db, rhs_ty)
188+
&& let (SemConstScalar::Bytes(lhs_bytes), SemConstScalar::Bytes(rhs_bytes)) =
189+
(&lhs_value, &rhs_value)
190+
{
191+
return fixed_string_bytes_eq(lhs_bytes.as_slice(), rhs_bytes.as_slice());
192+
}
193+
if lhs_ty != rhs_ty {
194+
return false;
195+
}
196+
lhs_value == rhs_value
197+
}
161198
(
162199
SemConstValue::TypeLevel {
163200
ty: lhs_ty,

crates/hir/src/analysis/semantic/ctfe/machine.rs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1963,15 +1963,43 @@ impl<'db> CtfeMachine<'db> {
19631963
let [value] = args else {
19641964
return Err(CtfeError::NotConstEvaluable { origin });
19651965
};
1966-
let bytes = self.const_as_bytes(value, origin)?;
1966+
let mut bytes = self.const_as_bytes(value, origin)?;
19671967
if let Some(len) = array_len(self.db, result_ty)
19681968
&& bytes.len() != len
19691969
{
1970-
return Err(CtfeError::NotConstEvaluable { origin });
1970+
if let Some(string_bytes) = self.fixed_string_bytes_for_len(value, len) {
1971+
bytes = string_bytes;
1972+
} else {
1973+
return Err(CtfeError::NotConstEvaluable { origin });
1974+
}
19711975
}
19721976
Ok(CtfeConstValue::bytes(result_ty, bytes))
19731977
}
19741978

1979+
fn fixed_string_bytes_for_len(
1980+
&self,
1981+
value: &CtfeConstValue<'db>,
1982+
len: usize,
1983+
) -> Option<Vec<u8>> {
1984+
let value = self.expand_interned(value.clone());
1985+
let CtfeConstKind::Bytes { ty, bytes } = &value.kind else {
1986+
return None;
1987+
};
1988+
if !ty.is_string(self.db) {
1989+
return None;
1990+
}
1991+
1992+
let mut out = vec![0u8; len];
1993+
let suffix = if bytes.len() > len {
1994+
&bytes[bytes.len() - len..]
1995+
} else {
1996+
bytes.as_ref()
1997+
};
1998+
let offset = len - suffix.len();
1999+
out[offset..].copy_from_slice(suffix);
2000+
Some(out)
2001+
}
2002+
19752003
fn eval_intrinsic_keccak(
19762004
&self,
19772005
result_ty: TyId<'db>,
@@ -2922,6 +2950,26 @@ impl<'db> CtfeMachine<'db> {
29222950
value: CtfeConstValue<'db>,
29232951
origin: SemOrigin<'db>,
29242952
) -> Result<CtfeValue<'db>, CtfeError<'db>> {
2953+
fn fixed_string_capacity_bytes<'db>(
2954+
db: &'db dyn HirAnalysisDb,
2955+
ty: TyId<'db>,
2956+
) -> Option<usize> {
2957+
if !ty.is_string(db) {
2958+
return None;
2959+
}
2960+
let (_, args) = ty.decompose_ty_app(db);
2961+
let len_ty = args.first().copied()?;
2962+
let TyData::ConstTy(const_ty) = len_ty.data(db) else {
2963+
return None;
2964+
};
2965+
match const_ty.data(db) {
2966+
ConstTyData::Evaluated(EvaluatedConstTy::LitInt(int_id), _) => {
2967+
int_id.data(db).to_usize()
2968+
}
2969+
_ => None,
2970+
}
2971+
}
2972+
29252973
let value = self.expand_interned(value);
29262974
match &value.kind {
29272975
CtfeConstKind::Bool(value) if int_ty_shape(self.db, result_ty).is_some() => {
@@ -2941,6 +2989,36 @@ impl<'db> CtfeMachine<'db> {
29412989
CtfeConstKind::Int { value, .. } if int_ty_shape(self.db, result_ty).is_some() => Ok(
29422990
CtfeValue::Value(CtfeConstValue::int(self.db, result_ty, value.to_bigint())),
29432991
),
2992+
CtfeConstKind::Int { value, .. } if result_ty.is_string(self.db) => {
2993+
fixed_string_capacity_bytes(self.db, result_ty)
2994+
.ok_or(CtfeError::NotConstEvaluable { origin })?;
2995+
let word = value.to_u256();
2996+
Ok(CtfeValue::Value(CtfeConstValue::bytes(
2997+
result_ty,
2998+
word.to_be_bytes::<32>().to_vec(),
2999+
)))
3000+
}
3001+
CtfeConstKind::Bytes { bytes, .. }
3002+
if matches!(int_ty_shape(self.db, result_ty), Some((_, false))) =>
3003+
{
3004+
let Some((bits, false)) = int_ty_shape(self.db, result_ty) else {
3005+
unreachable!("match guard should ensure unsigned int shape");
3006+
};
3007+
let width = usize::from(bits / 8);
3008+
if bytes.len() > width && bytes[..bytes.len() - width].iter().any(|byte| *byte != 0)
3009+
{
3010+
return Err(CtfeError::NotConstEvaluable { origin });
3011+
}
3012+
let suffix = if bytes.len() > width {
3013+
&bytes[bytes.len() - width..]
3014+
} else {
3015+
bytes.as_ref()
3016+
};
3017+
let value = BigInt::from(BigUint::from_bytes_be(suffix));
3018+
Ok(CtfeValue::Value(CtfeConstValue::int(
3019+
self.db, result_ty, value,
3020+
)))
3021+
}
29443022
CtfeConstKind::Bytes { bytes, .. } => Ok(CtfeValue::Value(CtfeConstValue::bytes(
29453023
result_ty,
29463024
bytes.to_vec(),

crates/hir/src/analysis/semantic/lower/body.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use cranelift_entity::EntityRef;
22
use num_bigint::BigInt;
3+
use num_traits::ToPrimitive;
34
use rustc_hash::FxHashMap;
45

56
use crate::{
@@ -16,14 +17,14 @@ use crate::{
1617
runtime_size_bytes, sem_const_from_ty, unit_const,
1718
},
1819
ty::{
19-
const_ty::const_ty_or_abstract_from_assoc_const_use,
20+
const_ty::{ConstTyData, EvaluatedConstTy, const_ty_or_abstract_from_assoc_const_use},
2021
normalize::normalize_ty,
2122
ty_check::{
2223
BodyOwner, CodeRegionIntrinsicKind, ConstIntrinsicKind, ConstRef, LocalBinding,
2324
PathReadSemantics, RecordInitLowering, RecordLike, SemanticExprLowering, TypedBody,
2425
ValuePathRef,
2526
},
26-
ty_def::{BorrowKind, TyId},
27+
ty_def::{BorrowKind, TyData, TyId},
2728
},
2829
},
2930
hir_def::{
@@ -194,6 +195,23 @@ pub(super) struct LoopScope {
194195
}
195196

196197
impl<'a, 'db> SmirLowerCtxt<'a, 'db> {
198+
pub(super) fn fixed_string_capacity_bytes(&self, ty: TyId<'db>) -> Option<usize> {
199+
if !ty.is_string(self.db) {
200+
return None;
201+
}
202+
let (_, args) = ty.decompose_ty_app(self.db);
203+
let len_ty = args.first().copied()?;
204+
let TyData::ConstTy(const_ty) = len_ty.data(self.db) else {
205+
return None;
206+
};
207+
match const_ty.data(self.db) {
208+
ConstTyData::Evaluated(EvaluatedConstTy::LitInt(int_id), _) => {
209+
int_id.data(self.db).to_usize()
210+
}
211+
_ => None,
212+
}
213+
}
214+
197215
fn new(
198216
db: &'db dyn HirAnalysisDb,
199217
instance: SemanticInstance<'db>,
@@ -590,7 +608,15 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> {
590608
let value = match lit {
591609
LitKind::Int(int_id) => int_const(self.db, ty, int_id.data(self.db).clone().into()),
592610
LitKind::String(string_id) => {
593-
bytes_const(self.db, ty, string_id.data(self.db).as_bytes().to_vec())
611+
let mut bytes = string_id.data(self.db).as_bytes().to_vec();
612+
if let Some(capacity) = self.fixed_string_capacity_bytes(ty)
613+
&& bytes.len() < capacity
614+
{
615+
let mut padded = vec![0u8; capacity - bytes.len()];
616+
padded.extend(bytes);
617+
bytes = padded;
618+
}
619+
bytes_const(self.db, ty, bytes)
594620
}
595621
LitKind::Bool(value) => bool_const(self.db, *value),
596622
};

crates/hir/src/analysis/semantic/lower/pattern.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,15 @@ impl<'a, 'db> SmirLowerCtxt<'a, 'db> {
647647
int_const(self.db, ty, BigInt::from(int_id.data(self.db).clone()))
648648
}
649649
LitKind::String(string_id) => {
650-
bytes_const(self.db, ty, string_id.data(self.db).as_bytes().to_vec())
650+
let mut bytes = string_id.data(self.db).as_bytes().to_vec();
651+
if let Some(capacity) = self.fixed_string_capacity_bytes(ty)
652+
&& bytes.len() < capacity
653+
{
654+
let mut padded = vec![0u8; capacity - bytes.len()];
655+
padded.extend(bytes);
656+
bytes = padded;
657+
}
658+
bytes_const(self.db, ty, bytes)
651659
}
652660
LitKind::Bool(value) => bool_const(self.db, value),
653661
};

crates/hir/src/analysis/ty/ty_check/expr.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,11 @@ impl<'db> TyChecker<'db> {
508508
inner_expr.data(self.db, self.body())
509509
{
510510
let value = int_id.data(self.db);
511+
if to.is_string(self.db) && self.int_literal_fits_in_ty(value, TyId::u256(self.db)) {
512+
let _ = self.table.unify(from, TyId::u256(self.db));
513+
return ExprProp::new(to, true);
514+
}
515+
511516
if self.int_literal_fits_in_ty(value, to) {
512517
// Unify the literal's type variable with the target leaf type
513518
// so it doesn't remain unresolved.

0 commit comments

Comments
 (0)