mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-09-30 22:01:37 +00:00
Fix size_of_val and support min_align_of_val
This commit is contained in:
parent
537f9b311d
commit
171ae2ee5d
7 changed files with 221 additions and 89 deletions
|
@ -282,8 +282,10 @@ pub mod known {
|
||||||
alloc,
|
alloc,
|
||||||
iter,
|
iter,
|
||||||
ops,
|
ops,
|
||||||
|
fmt,
|
||||||
future,
|
future,
|
||||||
result,
|
result,
|
||||||
|
string,
|
||||||
boxed,
|
boxed,
|
||||||
option,
|
option,
|
||||||
prelude,
|
prelude,
|
||||||
|
@ -311,6 +313,7 @@ pub mod known {
|
||||||
RangeToInclusive,
|
RangeToInclusive,
|
||||||
RangeTo,
|
RangeTo,
|
||||||
Range,
|
Range,
|
||||||
|
String,
|
||||||
Neg,
|
Neg,
|
||||||
Not,
|
Not,
|
||||||
None,
|
None,
|
||||||
|
@ -321,6 +324,7 @@ pub mod known {
|
||||||
iter_mut,
|
iter_mut,
|
||||||
len,
|
len,
|
||||||
is_empty,
|
is_empty,
|
||||||
|
as_str,
|
||||||
new,
|
new,
|
||||||
// Builtin macros
|
// Builtin macros
|
||||||
asm,
|
asm,
|
||||||
|
@ -334,6 +338,7 @@ pub mod known {
|
||||||
core_panic,
|
core_panic,
|
||||||
env,
|
env,
|
||||||
file,
|
file,
|
||||||
|
format,
|
||||||
format_args_nl,
|
format_args_nl,
|
||||||
format_args,
|
format_args,
|
||||||
global_asm,
|
global_asm,
|
||||||
|
|
|
@ -2456,20 +2456,6 @@ fn const_trait_assoc() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn panic_messages() {
|
|
||||||
check_fail(
|
|
||||||
r#"
|
|
||||||
//- minicore: panic
|
|
||||||
const GOAL: u8 = {
|
|
||||||
let x: u16 = 2;
|
|
||||||
panic!("hello");
|
|
||||||
};
|
|
||||||
"#,
|
|
||||||
|e| e == ConstEvalError::MirEvalError(MirEvalError::Panic("hello".to_string())),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exec_limits() {
|
fn exec_limits() {
|
||||||
check_fail(
|
check_fail(
|
||||||
|
|
|
@ -43,6 +43,50 @@ fn size_of_val() {
|
||||||
"#,
|
"#,
|
||||||
12,
|
12,
|
||||||
);
|
);
|
||||||
|
check_number(
|
||||||
|
r#"
|
||||||
|
//- minicore: coerce_unsized, transmute
|
||||||
|
use core::mem::transmute;
|
||||||
|
|
||||||
|
extern "rust-intrinsic" {
|
||||||
|
pub fn size_of_val<T: ?Sized>(_: *const T) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct X {
|
||||||
|
x: i64,
|
||||||
|
y: u8,
|
||||||
|
t: [i32],
|
||||||
|
}
|
||||||
|
|
||||||
|
const GOAL: usize = unsafe {
|
||||||
|
let y: &X = transmute([0usize, 3]);
|
||||||
|
size_of_val(y)
|
||||||
|
};
|
||||||
|
"#,
|
||||||
|
24,
|
||||||
|
);
|
||||||
|
check_number(
|
||||||
|
r#"
|
||||||
|
//- minicore: coerce_unsized, transmute
|
||||||
|
use core::mem::transmute;
|
||||||
|
|
||||||
|
extern "rust-intrinsic" {
|
||||||
|
pub fn size_of_val<T: ?Sized>(_: *const T) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct X {
|
||||||
|
x: i32,
|
||||||
|
y: i64,
|
||||||
|
t: [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
const GOAL: usize = unsafe {
|
||||||
|
let y: &X = transmute([0usize, 15]);
|
||||||
|
size_of_val(y)
|
||||||
|
};
|
||||||
|
"#,
|
||||||
|
32,
|
||||||
|
);
|
||||||
check_number(
|
check_number(
|
||||||
r#"
|
r#"
|
||||||
//- minicore: coerce_unsized, fmt, builtin_impls
|
//- minicore: coerce_unsized, fmt, builtin_impls
|
||||||
|
@ -74,6 +118,37 @@ fn size_of_val() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn min_align_of_val() {
|
||||||
|
check_number(
|
||||||
|
r#"
|
||||||
|
//- minicore: coerce_unsized
|
||||||
|
extern "rust-intrinsic" {
|
||||||
|
pub fn min_align_of_val<T: ?Sized>(_: *const T) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct X(i32, u8);
|
||||||
|
|
||||||
|
const GOAL: usize = min_align_of_val(&X(1, 2));
|
||||||
|
"#,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
check_number(
|
||||||
|
r#"
|
||||||
|
//- minicore: coerce_unsized
|
||||||
|
extern "rust-intrinsic" {
|
||||||
|
pub fn min_align_of_val<T: ?Sized>(_: *const T) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GOAL: usize = {
|
||||||
|
let x: &[i32] = &[1, 2, 3];
|
||||||
|
min_align_of_val(x)
|
||||||
|
};
|
||||||
|
"#,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transmute() {
|
fn transmute() {
|
||||||
check_number(
|
check_number(
|
||||||
|
|
|
@ -105,7 +105,7 @@ pub fn layout_of_adt_query(
|
||||||
&& variants
|
&& variants
|
||||||
.iter()
|
.iter()
|
||||||
.next()
|
.next()
|
||||||
.and_then(|x| x.last().map(|x| x.is_unsized()))
|
.and_then(|x| x.last().map(|x| !x.is_unsized()))
|
||||||
.unwrap_or(true),
|
.unwrap_or(true),
|
||||||
)
|
)
|
||||||
.ok_or(LayoutError::SizeOverflow)?
|
.ok_or(LayoutError::SizeOverflow)?
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
|
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
|
use chalk_ir::TyKind;
|
||||||
|
use hir_def::resolver::HasResolver;
|
||||||
|
use hir_expand::mod_path::ModPath;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
mod simd;
|
mod simd;
|
||||||
|
@ -186,44 +190,24 @@ impl Evaluator<'_> {
|
||||||
BeginPanic => Err(MirEvalError::Panic("<unknown-panic-payload>".to_string())),
|
BeginPanic => Err(MirEvalError::Panic("<unknown-panic-payload>".to_string())),
|
||||||
PanicFmt => {
|
PanicFmt => {
|
||||||
let message = (|| {
|
let message = (|| {
|
||||||
let arguments_struct =
|
let x = self.db.crate_def_map(self.crate_id).crate_root();
|
||||||
self.db.lang_item(self.crate_id, LangItem::FormatArguments)?.as_struct()?;
|
let resolver = x.resolver(self.db.upcast());
|
||||||
let arguments_layout = self
|
let Some(format_fn) = resolver.resolve_path_in_value_ns_fully(
|
||||||
.layout_adt(arguments_struct.into(), Substitution::empty(Interner))
|
self.db.upcast(),
|
||||||
.ok()?;
|
&hir_def::path::Path::from_known_path_with_no_generic(ModPath::from_segments(
|
||||||
let arguments_field_pieces =
|
hir_expand::mod_path::PathKind::Abs,
|
||||||
self.db.struct_data(arguments_struct).variant_data.field(&name![pieces])?;
|
[name![std], name![fmt], name![format]].into_iter(),
|
||||||
let pieces_offset = arguments_layout
|
)),
|
||||||
.fields
|
) else {
|
||||||
.offset(u32::from(arguments_field_pieces.into_raw()) as usize)
|
not_supported!("std::fmt::format not found");
|
||||||
.bytes_usize();
|
};
|
||||||
let ptr_size = self.ptr_size();
|
let hir_def::resolver::ValueNs::FunctionId(format_fn) = format_fn else { not_supported!("std::fmt::format is not a function") };
|
||||||
let arg = args.next()?;
|
let message_string = self.interpret_mir(&*self.db.mir_body(format_fn.into()).map_err(|e| MirEvalError::MirLowerError(format_fn, e))?, args.cloned())?;
|
||||||
let pieces_array_addr =
|
let addr = Address::from_bytes(&message_string[self.ptr_size()..2 * self.ptr_size()])?;
|
||||||
Address::from_bytes(&arg[pieces_offset..pieces_offset + ptr_size]).ok()?;
|
let size = from_bytes!(usize, message_string[2 * self.ptr_size()..]);
|
||||||
let pieces_array_len = usize::from_le_bytes(
|
Ok(std::string::String::from_utf8_lossy(self.read_memory(addr, size)?).into_owned())
|
||||||
(&arg[pieces_offset + ptr_size..pieces_offset + 2 * ptr_size])
|
|
||||||
.try_into()
|
|
||||||
.ok()?,
|
|
||||||
);
|
|
||||||
let mut message = "".to_string();
|
|
||||||
for i in 0..pieces_array_len {
|
|
||||||
let piece_ptr_addr = pieces_array_addr.offset(2 * i * ptr_size);
|
|
||||||
let piece_addr =
|
|
||||||
Address::from_bytes(self.read_memory(piece_ptr_addr, ptr_size).ok()?)
|
|
||||||
.ok()?;
|
|
||||||
let piece_len = usize::from_le_bytes(
|
|
||||||
self.read_memory(piece_ptr_addr.offset(ptr_size), ptr_size)
|
|
||||||
.ok()?
|
|
||||||
.try_into()
|
|
||||||
.ok()?,
|
|
||||||
);
|
|
||||||
let piece_data = self.read_memory(piece_addr, piece_len).ok()?;
|
|
||||||
message += &std::string::String::from_utf8_lossy(piece_data);
|
|
||||||
}
|
|
||||||
Some(message)
|
|
||||||
})()
|
})()
|
||||||
.unwrap_or_else(|| "<format-args-evaluation-failed>".to_string());
|
.unwrap_or_else(|e| format!("Failed to render panic format args: {e:?}"));
|
||||||
Err(MirEvalError::Panic(message))
|
Err(MirEvalError::Panic(message))
|
||||||
}
|
}
|
||||||
SliceLen => {
|
SliceLen => {
|
||||||
|
@ -544,6 +528,13 @@ impl Evaluator<'_> {
|
||||||
let size = self.size_of_sized(ty, locals, "size_of arg")?;
|
let size = self.size_of_sized(ty, locals, "size_of arg")?;
|
||||||
destination.write_from_bytes(self, &size.to_le_bytes()[0..destination.size])
|
destination.write_from_bytes(self, &size.to_le_bytes()[0..destination.size])
|
||||||
}
|
}
|
||||||
|
"min_align_of" | "pref_align_of" => {
|
||||||
|
let Some(ty) = generic_args.as_slice(Interner).get(0).and_then(|x| x.ty(Interner)) else {
|
||||||
|
return Err(MirEvalError::TypeError("align_of generic arg is not provided"));
|
||||||
|
};
|
||||||
|
let align = self.layout(ty)?.align.abi.bytes();
|
||||||
|
destination.write_from_bytes(self, &align.to_le_bytes()[0..destination.size])
|
||||||
|
}
|
||||||
"size_of_val" => {
|
"size_of_val" => {
|
||||||
let Some(ty) = generic_args.as_slice(Interner).get(0).and_then(|x| x.ty(Interner))
|
let Some(ty) = generic_args.as_slice(Interner).get(0).and_then(|x| x.ty(Interner))
|
||||||
else {
|
else {
|
||||||
|
@ -552,33 +543,28 @@ impl Evaluator<'_> {
|
||||||
let [arg] = args else {
|
let [arg] = args else {
|
||||||
return Err(MirEvalError::TypeError("size_of_val args are not provided"));
|
return Err(MirEvalError::TypeError("size_of_val args are not provided"));
|
||||||
};
|
};
|
||||||
let metadata = arg.interval.slice(self.ptr_size()..self.ptr_size() * 2);
|
if let Some((size, _)) = self.size_align_of(ty, locals)? {
|
||||||
let size = match ty.kind(Interner) {
|
destination.write_from_bytes(self, &size.to_le_bytes())
|
||||||
TyKind::Str => return destination.write_from_interval(self, metadata),
|
} else {
|
||||||
TyKind::Slice(inner) => {
|
let metadata = arg.interval.slice(self.ptr_size()..self.ptr_size() * 2);
|
||||||
let len = from_bytes!(usize, metadata.get(self)?);
|
let (size, _) = self.size_align_of_unsized(ty, metadata, locals)?;
|
||||||
len * self.size_of_sized(inner, locals, "slice inner type")?
|
destination.write_from_bytes(self, &size.to_le_bytes())
|
||||||
}
|
}
|
||||||
TyKind::Dyn(_) => self.size_of_sized(
|
|
||||||
self.vtable_map.ty_of_bytes(metadata.get(self)?)?,
|
|
||||||
locals,
|
|
||||||
"dyn concrete type",
|
|
||||||
)?,
|
|
||||||
_ => self.size_of_sized(
|
|
||||||
ty,
|
|
||||||
locals,
|
|
||||||
"unsized type other than str, slice, and dyn",
|
|
||||||
)?,
|
|
||||||
};
|
|
||||||
destination.write_from_bytes(self, &size.to_le_bytes())
|
|
||||||
}
|
}
|
||||||
"min_align_of" | "pref_align_of" => {
|
"min_align_of_val" => {
|
||||||
let Some(ty) = generic_args.as_slice(Interner).get(0).and_then(|x| x.ty(Interner))
|
let Some(ty) = generic_args.as_slice(Interner).get(0).and_then(|x| x.ty(Interner)) else {
|
||||||
else {
|
return Err(MirEvalError::TypeError("min_align_of_val generic arg is not provided"));
|
||||||
return Err(MirEvalError::TypeError("align_of generic arg is not provided"));
|
|
||||||
};
|
};
|
||||||
let align = self.layout(ty)?.align.abi.bytes();
|
let [arg] = args else {
|
||||||
destination.write_from_bytes(self, &align.to_le_bytes()[0..destination.size])
|
return Err(MirEvalError::TypeError("min_align_of_val args are not provided"));
|
||||||
|
};
|
||||||
|
if let Some((_, align)) = self.size_align_of(ty, locals)? {
|
||||||
|
destination.write_from_bytes(self, &align.to_le_bytes())
|
||||||
|
} else {
|
||||||
|
let metadata = arg.interval.slice(self.ptr_size()..self.ptr_size() * 2);
|
||||||
|
let (_, align) = self.size_align_of_unsized(ty, metadata, locals)?;
|
||||||
|
destination.write_from_bytes(self, &align.to_le_bytes())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"needs_drop" => {
|
"needs_drop" => {
|
||||||
let Some(ty) = generic_args.as_slice(Interner).get(0).and_then(|x| x.ty(Interner))
|
let Some(ty) = generic_args.as_slice(Interner).get(0).and_then(|x| x.ty(Interner))
|
||||||
|
@ -905,6 +891,58 @@ impl Evaluator<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn size_align_of_unsized(
|
||||||
|
&mut self,
|
||||||
|
ty: &Ty,
|
||||||
|
metadata: Interval,
|
||||||
|
locals: &Locals<'_>,
|
||||||
|
) -> Result<(usize, usize)> {
|
||||||
|
Ok(match ty.kind(Interner) {
|
||||||
|
TyKind::Str => (from_bytes!(usize, metadata.get(self)?), 1),
|
||||||
|
TyKind::Slice(inner) => {
|
||||||
|
let len = from_bytes!(usize, metadata.get(self)?);
|
||||||
|
let (size, align) = self.size_align_of_sized(inner, locals, "slice inner type")?;
|
||||||
|
(size * len, align)
|
||||||
|
}
|
||||||
|
TyKind::Dyn(_) => self.size_align_of_sized(
|
||||||
|
self.vtable_map.ty_of_bytes(metadata.get(self)?)?,
|
||||||
|
locals,
|
||||||
|
"dyn concrete type",
|
||||||
|
)?,
|
||||||
|
TyKind::Adt(id, subst) => {
|
||||||
|
let id = id.0;
|
||||||
|
let layout = self.layout_adt(id, subst.clone())?;
|
||||||
|
let id = match id {
|
||||||
|
AdtId::StructId(s) => s,
|
||||||
|
_ => not_supported!("unsized enum or union"),
|
||||||
|
};
|
||||||
|
let field_types = &self.db.field_types(id.into());
|
||||||
|
let last_field_ty =
|
||||||
|
field_types.iter().rev().next().unwrap().1.clone().substitute(Interner, subst);
|
||||||
|
let sized_part_size =
|
||||||
|
layout.fields.offset(field_types.iter().count() - 1).bytes_usize();
|
||||||
|
let sized_part_align = layout.align.abi.bytes() as usize;
|
||||||
|
let (unsized_part_size, unsized_part_align) =
|
||||||
|
self.size_align_of_unsized(&last_field_ty, metadata, locals)?;
|
||||||
|
let align = sized_part_align.max(unsized_part_align) as isize;
|
||||||
|
let size = (sized_part_size + unsized_part_size) as isize;
|
||||||
|
// Must add any necessary padding to `size`
|
||||||
|
// (to make it a multiple of `align`) before returning it.
|
||||||
|
//
|
||||||
|
// Namely, the returned size should be, in C notation:
|
||||||
|
//
|
||||||
|
// `size + ((size & (align-1)) ? align : 0)`
|
||||||
|
//
|
||||||
|
// emulated via the semi-standard fast bit trick:
|
||||||
|
//
|
||||||
|
// `(size + (align-1)) & -align`
|
||||||
|
let size = (size + (align - 1)) & (-align);
|
||||||
|
(size as usize, align as usize)
|
||||||
|
}
|
||||||
|
_ => not_supported!("unsized type other than str, slice, struct and dyn"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn exec_atomic_intrinsic(
|
fn exec_atomic_intrinsic(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
//! Shim implementation for simd intrinsics
|
//! Shim implementation for simd intrinsics
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use crate::TyKind;
|
use crate::TyKind;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -22,10 +24,15 @@ macro_rules! not_supported {
|
||||||
impl Evaluator<'_> {
|
impl Evaluator<'_> {
|
||||||
fn detect_simd_ty(&self, ty: &Ty) -> Result<usize> {
|
fn detect_simd_ty(&self, ty: &Ty) -> Result<usize> {
|
||||||
match ty.kind(Interner) {
|
match ty.kind(Interner) {
|
||||||
TyKind::Adt(_, subst) => {
|
TyKind::Adt(id, subst) => {
|
||||||
let Some(len) = subst.as_slice(Interner).get(1).and_then(|x| x.constant(Interner))
|
let len = match subst.as_slice(Interner).get(1).and_then(|x| x.constant(Interner)) {
|
||||||
else {
|
Some(len) => len,
|
||||||
return Err(MirEvalError::TypeError("simd type without len param"));
|
_ => {
|
||||||
|
if let AdtId::StructId(id) = id.0 {
|
||||||
|
return Ok(self.db.struct_data(id).variant_data.fields().len());
|
||||||
|
}
|
||||||
|
return Err(MirEvalError::TypeError("simd type with no len param"));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match try_const_usize(self.db, len) {
|
match try_const_usize(self.db, len) {
|
||||||
Some(x) => Ok(x as usize),
|
Some(x) => Ok(x as usize),
|
||||||
|
@ -63,13 +70,34 @@ impl Evaluator<'_> {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
destination.write_from_bytes(self, &result)
|
destination.write_from_bytes(self, &result)
|
||||||
}
|
}
|
||||||
"eq" | "ne" => {
|
"eq" | "ne" | "lt" | "le" | "gt" | "ge" => {
|
||||||
let [left, right] = args else {
|
let [left, right] = args else {
|
||||||
return Err(MirEvalError::TypeError("simd_eq args are not provided"));
|
return Err(MirEvalError::TypeError("simd args are not provided"));
|
||||||
};
|
};
|
||||||
let result = left.get(self)? == right.get(self)?;
|
let len = self.detect_simd_ty(&left.ty)?;
|
||||||
let result = result ^ (name == "ne");
|
let size = left.interval.size / len;
|
||||||
destination.write_from_bytes(self, &[u8::from(result)])
|
let dest_size = destination.size / len;
|
||||||
|
let mut destination_bytes = vec![];
|
||||||
|
let vector = left.get(self)?.chunks(size).zip(right.get(self)?.chunks(size));
|
||||||
|
for (l, r) in vector {
|
||||||
|
let mut result = Ordering::Equal;
|
||||||
|
for (l, r) in l.iter().zip(r).rev() {
|
||||||
|
let x = l.cmp(r);
|
||||||
|
if x != Ordering::Equal {
|
||||||
|
result = x;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let result = match result {
|
||||||
|
Ordering::Less => ["lt", "le", "ne"].contains(&name),
|
||||||
|
Ordering::Equal => ["ge", "le", "eq"].contains(&name),
|
||||||
|
Ordering::Greater => ["ge", "gt", "ne"].contains(&name),
|
||||||
|
};
|
||||||
|
let result = if result { 255 } else { 0 };
|
||||||
|
destination_bytes.extend(std::iter::repeat(result).take(dest_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
destination.write_from_bytes(self, &destination_bytes)
|
||||||
}
|
}
|
||||||
"bitmask" => {
|
"bitmask" => {
|
||||||
let [op] = args else {
|
let [op] = args else {
|
||||||
|
@ -103,9 +131,9 @@ impl Evaluator<'_> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let left_len = self.detect_simd_ty(&left.ty)?;
|
let left_len = self.detect_simd_ty(&left.ty)?;
|
||||||
let left_count = left.interval.size / left_len;
|
let left_size = left.interval.size / left_len;
|
||||||
let vector =
|
let vector =
|
||||||
left.get(self)?.chunks(left_count).chain(right.get(self)?.chunks(left_count));
|
left.get(self)?.chunks(left_size).chain(right.get(self)?.chunks(left_size));
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
for index in index.get(self)?.chunks(index.interval.size / index_len) {
|
for index in index.get(self)?.chunks(index.interval.size / index_len) {
|
||||||
let index = from_bytes!(u32, index) as usize;
|
let index = from_bytes!(u32, index) as usize;
|
||||||
|
|
|
@ -674,7 +674,7 @@ struct Foo { fiel$0d_a: u8, field_b: i32, field_c: i16 }
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
field_a: u8 // size = 1, align = 1, offset = 4
|
field_a: u8 // size = 1, align = 1, offset = 6
|
||||||
```
|
```
|
||||||
"#]],
|
"#]],
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue