mod asm;
mod atomics;
mod builtins;
mod decl;
mod expr;
mod float128;
mod init;
mod long_double;
mod stmt;
mod types;
mod va;
mod vector;
pub use atomics::is_atomic_builtin;
pub use long_double::long_double_twin;
use std::collections::{HashMap, HashSet};
use crate::Options;
use crate::ast;
use crate::capture::SourceRange;
use crate::diag::{Diagnostic, Diagnostics};
use crate::ir::{
self, ConstValue, Expr, ExprKind, FuncId, INT128_TYPEDEF_NAMES, LoopId, ObjectId, Place,
PlaceKind, Program, RecordId, Signature, Storage, SwitchId, Ty, Types, VA_LIST_NAMES,
X86_VECTOR_TYPEDEF_NAMES,
};
use crate::target::{Arch, TargetModel};
pub fn analyze(
unit: &ast::TranslationUnit,
options: &Options,
unit_id: u64,
) -> (Program, Diagnostics) {
let mut sema = Sema::new(unit, options, unit_id);
sema.run(unit);
let Sema { diags, program, .. } = sema;
(program, diags)
}
pub fn check_pragmas(program: &Program) -> Diagnostics {
let mut diags = Diagnostics::new();
if program.no_std {
for range in &program.cpu_supports {
diags.error(
*range,
"'__builtin_cpu_supports' requires std; this unit says no_std. It becomes \
`std::is_x86_feature_detected!`, and `core` has no processor detection — \
`cpuid` is not something a library can do without one. Ask for the \
instruction set with __attribute__((target(\"…\"))) and let the caller \
guarantee it, or drop the no_std pragma"
.to_owned(),
);
}
}
if !program.no_std && !program.export {
return diags;
}
for object in &program.objects {
let Storage::ThreadLocal { exported, .. } = &object.storage else {
continue;
};
if program.no_std {
diags.error(
object.range,
"'_Thread_local' requires std; this unit says no_std. Rust's `thread_local!` \
is a `std` macro, and `core` has no thread-local storage"
.to_owned(),
);
}
if program.export && *exported {
diags.error(
object.range,
"a '_Thread_local' object cannot be exported: `#pragma cinrs export` gives an \
item a C symbol, and there is no stable way to give one to a `thread_local!`"
.to_owned(),
);
}
}
diags
}
pub(super) fn list_of_names(names: &[&str]) -> String {
let quoted: Vec<String> = names.iter().map(|n| format!("'{n}'")).collect();
match quoted.split_last() {
Some((last, [])) => last.clone(),
Some((last, rest)) => format!("{} and {last}", rest.join(", ")),
None => String::new(),
}
}
pub fn check_safe(program: &mut Program, named: &[crate::pp::SafeName]) -> Diagnostics {
let mut diags = Diagnostics::new();
for request in named {
let found = program
.functions
.iter_mut()
.find(|func| func.name == request.name && !func.is_nested());
match found {
Some(func) => func.safe = func.safe.or(Some(request.range)),
None => diags.error(
request.range,
format!(
"#pragma cinrs safe names '{}', which this unit does not declare",
request.name
),
),
}
}
let mut refused: HashSet<usize> = HashSet::new();
for (index, func) in program.functions.iter_mut().enumerate() {
let Some(range) = func.safe else { continue };
let refusal = if func.body.is_none() {
format!(
"'{}' is only declared here, so it cannot be safe: an extern function is \
compiled elsewhere and nothing about it can be checked",
func.name
)
} else if func.sig.variadic {
format!(
"'{}' takes '...', so it cannot be safe: Rust makes every function with a \
C variable argument list unsafe",
func.name
)
} else if func.is_nested() {
format!(
"'{}' is a nested function, so it cannot be safe: it reaches the enclosing \
function's objects through hidden pointer parameters, which its body \
dereferences",
func.name
)
} else if let Some(feature) = func.target_features.first() {
format!(
"'{}' asks for the '{feature}' instruction set, so it cannot be safe: Rust makes \
a '#[target_feature]' function unsafe to call from anywhere that does not have \
that instruction set, which is the opposite of what [[cinrs::safe]] promises. \
Whether the processor really has it is something only the caller knows — \
'__builtin_cpu_supports' is how to ask",
func.name
)
} else {
continue;
};
func.safe = None;
refused.insert(index);
diags.error(range, refusal);
}
for call in &program.calls {
let caller = program.function(call.caller);
let callee = program.function(call.callee);
if !caller.is_safe() {
continue;
}
if let Some(intr) = callee.intrinsic {
let feature = crate::x86::describe_features(intr.feature);
diags.error(
call.range,
format!(
"'{}' needs {feature}, so it cannot be called from the safe function '{}': \
Rust makes every '#[target_feature]' function unsafe to call, because only \
the program knows whether the processor running it has those instructions. \
Drop [[cinrs::safe]] from '{}' and let its Rust caller write 'unsafe'",
intr.name, caller.name, caller.name
),
);
continue;
}
if callee.is_safe() || callee.is_extern() || refused.contains(&(call.callee.0 as usize)) {
continue;
}
diags.error(
call.range,
format!(
"function '{}' is not safe; mark it [[cinrs::safe]] or call it from a non-safe \
function",
callee.name
),
);
}
diags
}
#[derive(Clone, Debug)]
struct VmBound {
object: Option<ObjectId>,
value: Expr,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum BoundMode {
Object,
Expression,
Unevaluated,
}
#[derive(Clone, Debug)]
enum Entry {
Object(ObjectId),
Function(FuncId),
Typedef(TypedefEntry),
Constant {
value: ConstValue,
ty: Ty,
range: SourceRange,
},
}
impl Entry {
fn describe(&self) -> &'static str {
match self {
Entry::Object(_) => "a variable",
Entry::Function(_) => "a function",
Entry::Typedef(_) => "a type",
Entry::Constant { .. } => "a constant",
}
}
}
#[derive(Clone, Debug)]
struct TypedefEntry {
resolved: Result<Ty, String>,
range: SourceRange,
align: Option<u64>,
}
#[derive(Clone, Copy, Debug)]
enum TagEntry {
Record(RecordId),
Enum {
ty: Ty,
unsigned: bool,
fixed: bool,
complete: bool,
list: Option<u32>,
},
}
#[derive(Default)]
struct Scope {
entries: HashMap<String, Entry>,
composites: HashMap<ObjectId, Ty>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Breakable {
Loop(LoopId),
Switch(SwitchId),
}
#[derive(Clone, Copy, Debug)]
struct Label {
id: ir::LabelId,
range: SourceRange,
}
struct SwitchState {
id: SwitchId,
ty: Ty,
seen: Vec<(ir::CaseRange, SourceRange)>,
default: Option<SourceRange>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum FuncScope {
File,
Nested,
}
struct NestFrame {
func: FuncId,
labels: HashSet<String>,
}
struct SavedFunc {
ret_ty: Ty,
func_name: String,
func_variadic: bool,
func_params: Vec<ObjectId>,
va_param: Option<ObjectId>,
cfg_mode: bool,
region_labels: HashSet<String>,
labels: HashMap<String, Label>,
breakables: Vec<Breakable>,
switch_stack: Vec<SwitchState>,
vla_scopes: Vec<ObjectId>,
label_vla_scopes: HashMap<ir::LabelId, Vec<ObjectId>>,
goto_scopes: Vec<(SourceRange, ir::LabelId, Vec<ObjectId>)>,
switch_vla_depths: Vec<usize>,
func_uses_arena: bool,
cleanup_depth: usize,
next_loop: u32,
next_switch: u32,
}
struct TypeError {
range: SourceRange,
message: String,
note: Option<(SourceRange, String)>,
}
impl TypeError {
fn at(range: SourceRange, message: impl Into<String>) -> Self {
Self {
range,
message: message.into(),
note: None,
}
}
fn silent(range: SourceRange) -> Self {
Self::at(range, String::new())
}
}
struct Sema<'a> {
unit: &'a ast::TranslationUnit,
diags: Diagnostics,
complex: bool,
gating: crate::Gating,
target: TargetModel,
program: Program,
scopes: Vec<Scope>,
tags: Vec<HashMap<String, TagEntry>>,
record_by_spec: Vec<Option<RecordId>>,
enum_by_spec: Vec<Option<Ty>>,
enum_unsigned: Vec<Option<bool>>,
enum_incomplete: Vec<Option<String>>,
enum_lists: Vec<Vec<(String, i128)>>,
defined_functions: HashSet<String>,
long_double_decls: HashMap<SourceRange, u8>,
long_double_exprs: HashMap<SourceRange, Option<u8>>,
long_double_funcs: HashMap<FuncId, long_double::LongDoubleSig>,
long_double_uses: Vec<long_double::LongDoubleUse>,
float128: [Option<ir::RecordId>; 2],
no_complex: [Option<ir::RecordId>; 2],
dead_code: u32,
item_names: HashSet<String>,
library_prototyped: HashSet<ir::FuncId>,
initialized: HashSet<ObjectId>,
compound_literals: Vec<ObjectId>,
rvalue_lanes: HashSet<ObjectId>,
vm_bounds: Vec<VmBound>,
bound_mode: BoundMode,
vm_name: String,
pinned_typedefs: Vec<(SourceRange, String, TypedefEntry)>,
incomplete_enum_objects: Vec<(String, String, SourceRange)>,
standalone_enum: Option<ast::EnumSpecId>,
underspecified: Option<String>,
proto_depth: usize,
param_tags: HashMap<String, TagEntry>,
in_param_type: bool,
vla_scopes: Vec<ObjectId>,
label_vla_scopes: HashMap<ir::LabelId, Vec<ObjectId>>,
goto_scopes: Vec<(SourceRange, ir::LabelId, Vec<ObjectId>)>,
switch_vla_depths: Vec<usize>,
func_uses_arena: bool,
pending_discard: Vec<Expr>,
cleanup_depth: usize,
static_literals: HashMap<ObjectId, usize>,
object_level: Vec<u32>,
nest: Vec<NestFrame>,
nest_chains: HashMap<FuncId, Vec<FuncId>>,
nested_calls: Vec<(FuncId, FuncId)>,
nested_addresses: Vec<(FuncId, SourceRange)>,
refused_captures: HashSet<(FuncId, ObjectId)>,
ret_ty: Ty,
func_name: String,
func_variadic: bool,
func_params: Vec<ObjectId>,
va_param: Option<ObjectId>,
cfg_mode: bool,
region_labels: HashSet<String>,
labels: HashMap<String, Label>,
flexible_init: init::FlexibleInit,
label_addrs: HashSet<ir::LabelId>,
breakables: Vec<Breakable>,
switch_stack: Vec<SwitchState>,
next_loop: u32,
next_switch: u32,
next_label: u32,
next_anon: u32,
}
impl<'a> Sema<'a> {
fn new(unit: &'a ast::TranslationUnit, options: &Options, unit_id: u64) -> Self {
let mut sema = Self {
unit,
diags: Diagnostics::new(),
complex: options.complex,
gating: options.gating(),
target: options.target,
program: Program {
unit_id,
crate_path: ir::DEFAULT_CRATE_PATH.to_owned(),
..Program::default()
},
scopes: vec![Scope::default()],
tags: vec![HashMap::new()],
record_by_spec: vec![None; unit.records.len()],
enum_by_spec: vec![None; unit.enums.len()],
enum_unsigned: vec![None; unit.enums.len()],
enum_incomplete: vec![None; unit.enums.len()],
enum_lists: Vec::new(),
defined_functions: HashSet::new(),
long_double_decls: HashMap::new(),
long_double_exprs: HashMap::new(),
long_double_funcs: HashMap::new(),
long_double_uses: Vec::new(),
float128: [None; 2],
no_complex: [None; 2],
dead_code: 0,
item_names: HashSet::new(),
library_prototyped: HashSet::new(),
initialized: HashSet::new(),
compound_literals: Vec::new(),
rvalue_lanes: HashSet::new(),
vm_bounds: Vec::new(),
bound_mode: BoundMode::Expression,
vm_name: String::new(),
pinned_typedefs: Vec::new(),
incomplete_enum_objects: Vec::new(),
standalone_enum: None,
underspecified: None,
proto_depth: 0,
param_tags: HashMap::new(),
in_param_type: false,
vla_scopes: Vec::new(),
label_vla_scopes: HashMap::new(),
goto_scopes: Vec::new(),
switch_vla_depths: Vec::new(),
func_uses_arena: false,
pending_discard: Vec::new(),
cleanup_depth: 0,
static_literals: HashMap::new(),
object_level: Vec::new(),
nest: Vec::new(),
nest_chains: HashMap::new(),
nested_calls: Vec::new(),
nested_addresses: Vec::new(),
refused_captures: HashSet::new(),
ret_ty: Ty::Void,
func_name: String::new(),
func_variadic: false,
func_params: Vec::new(),
va_param: None,
cfg_mode: false,
region_labels: HashSet::new(),
labels: HashMap::new(),
flexible_init: init::FlexibleInit::Automatic,
label_addrs: HashSet::new(),
breakables: Vec::new(),
switch_stack: Vec::new(),
next_loop: 0,
next_switch: 0,
next_label: 0,
next_anon: 0,
};
for name in VA_LIST_NAMES {
sema.scopes[0].entries.insert(
(*name).to_owned(),
Entry::Typedef(TypedefEntry {
resolved: Ok(Ty::VaList),
range: SourceRange::at(0),
align: None,
}),
);
}
for (name, ty) in INT128_TYPEDEF_NAMES {
sema.scopes[0].entries.insert(
(*name).to_owned(),
Entry::Typedef(TypedefEntry {
resolved: Ok(*ty),
range: SourceRange::at(0),
align: None,
}),
);
}
if matches!(sema.target.arch, Arch::X86 | Arch::X86_64) {
for (name, ty) in X86_VECTOR_TYPEDEF_NAMES {
sema.scopes[0].entries.insert(
(*name).to_owned(),
Entry::Typedef(TypedefEntry {
resolved: Ok(*ty),
range: SourceRange::at(0),
align: None,
}),
);
}
}
sema
}
fn run(&mut self, unit: &ast::TranslationUnit) {
for item in &unit.items {
if let ast::ExternalDecl::Function(f) = item {
self.defined_functions.insert(f.name.name.clone());
}
}
for item in &unit.items {
match item {
ast::ExternalDecl::Function(f) => self.function_def(f),
ast::ExternalDecl::Decl(d) => self.file_scope_decl(d),
ast::ExternalDecl::StaticAssert(assert) => self.static_assert(assert),
}
}
self.complete_tentative_arrays();
self.check_tentative_enums();
self.propagate_nested_env();
self.check_nested_addresses();
self.check_nested_definitions();
self.check_long_double_boundary();
}
fn complete_tentative_arrays(&mut self) {
for index in 0..self.program.statics.len() {
let id = self.program.statics[index].object;
let ty = self.program.object(id).ty;
let Some(completed) = self.program.types.complete_tentative_array(ty) else {
continue;
};
self.program.objects[id.0 as usize].ty = completed;
let range = self.program.object(id).range;
self.program.statics[index].init = self.zero(completed, range);
}
}
fn check_tentative_enums(&mut self) {
for (name, tag, range) in std::mem::take(&mut self.incomplete_enum_objects) {
if matches!(
self.tags.first().and_then(|scope| scope.get(&tag)),
Some(TagEntry::Enum { complete: true, .. })
) {
continue;
}
self.error(
range,
format!(
"the tentative definition of '{name}' has type 'enum {tag}', which is \
never completed"
),
);
}
}
fn error(&mut self, range: SourceRange, message: impl Into<String>) {
self.diags.error(range, message);
}
fn require_standard(&mut self, needed: crate::Standard, what: &str, range: SourceRange) {
if let Some(message) = self.gating.requires(what, needed) {
self.error(range, message);
}
}
fn gnu_leniency(&self) -> bool {
self.gating.dialect.is_gnu()
}
fn gnu_note(&self) -> String {
format!(
"GCC accepts this with a warning; write {} for the same leniency",
self.gating.standard.macro_name_in(crate::Dialect::Gnu)
)
}
fn newer_keyword(&self, name: &str) -> Option<String> {
self.gating.newer_keyword(name)
}
fn error_note(
&mut self,
range: SourceRange,
message: impl Into<String>,
note_range: SourceRange,
note: impl Into<String>,
) {
self.diags
.push(Diagnostic::error(range, message).with_note_at(note_range, note));
}
fn push_scope(&mut self) {
self.scopes.push(Scope::default());
self.tags.push(HashMap::new());
}
fn pop_scope(&mut self) {
self.scopes.pop();
self.tags.pop();
}
fn push_prototype_scope(&mut self) {
self.scopes.push(Scope::default());
self.tags.push(HashMap::new());
self.proto_depth += 1;
}
fn pop_prototype_scope(&mut self, carry: bool) {
self.scopes.pop();
let tags = self.tags.pop().unwrap_or_default();
self.param_tags = if carry { tags } else { HashMap::new() };
self.proto_depth -= 1;
}
fn at_file_scope(&self) -> bool {
self.scopes.len() - self.proto_depth == 1
}
fn lookup(&self, name: &str) -> Option<&Entry> {
self.scopes.iter().rev().find_map(|s| s.entries.get(name))
}
fn lookup_typedef(&self, name: &ast::Ident) -> Option<&TypedefEntry> {
let pinned = self
.pinned_typedefs
.iter()
.rev()
.find(|(range, pinned, _)| *range == name.range && *pinned == name.name);
match pinned {
Some((_, _, entry)) => Some(entry),
None => match self.lookup(&name.name) {
Some(Entry::Typedef(entry)) => Some(entry),
_ => None,
},
}
}
fn declared_here(&self, name: &str) -> Option<&Entry> {
self.scopes.last().and_then(|s| s.entries.get(name))
}
fn lookup_linked(&self, name: &str) -> Option<&Entry> {
self.scopes.first().and_then(|s| s.entries.get(name))
}
fn insert(&mut self, name: &str, entry: Entry) {
if let Some(scope) = self.scopes.last_mut() {
scope.entries.insert(name.to_owned(), entry);
}
}
fn insert_at_file_scope(&mut self, name: &str, entry: Entry) {
if let Some(scope) = self.scopes.first_mut() {
scope.entries.insert(name.to_owned(), entry);
}
}
fn note_object_ty(&mut self, id: ObjectId, ty: Ty) {
if let Some(scope) = self.scopes.last_mut() {
scope.composites.insert(id, ty);
}
}
fn visible_object_ty(&self, id: ObjectId) -> Ty {
self.scopes
.iter()
.rev()
.find_map(|scope| scope.composites.get(&id).copied())
.unwrap_or_else(|| self.program.object(id).ty)
}
fn lookup_tag(&self, name: &str) -> Option<TagEntry> {
self.tags.iter().rev().find_map(|s| s.get(name)).copied()
}
fn tag_here(&self, name: &str) -> Option<TagEntry> {
self.tags.last().and_then(|s| s.get(name)).copied()
}
fn insert_tag(&mut self, name: &str, entry: TagEntry) {
if let Some(scope) = self.tags.last_mut() {
scope.insert(name.to_owned(), entry);
}
}
fn take_param_tags(&mut self) {
let carried = std::mem::take(&mut self.param_tags);
if let Some(scope) = self.tags.last_mut() {
scope.extend(carried);
}
}
fn record_spec(&self, id: ast::RecordSpecId) -> &'a ast::RecordSpec {
self.unit.record(id)
}
fn enum_spec(&self, id: ast::EnumSpecId) -> &'a ast::EnumSpec {
self.unit.enum_spec(id)
}
fn typeof_operand(&self, id: ast::TypeofId) -> &'a ast::TypeofOperand {
self.unit.typeof_operand(id)
}
fn incomplete_enum(&self, ty: &ast::Type) -> Option<&str> {
let ast::TypeKind::Enum(id) = &ty.kind else {
return None;
};
self.enum_incomplete[id.index()].as_deref()
}
fn types(&self) -> &Types {
&self.program.types
}
fn tyname(&self, ty: Ty) -> String {
self.program.types.name(ty)
}
fn pointee(&self, ty: Ty) -> Option<Ty> {
self.program.types.pointee(ty)
}
fn ptr_to(&mut self, pointee: Ty, konst: bool) -> Ty {
let konst =
konst || matches!(pointee, Ty::Array(id) if self.types().array_type(id).elem_const);
self.program.types.pointer(pointee, konst)
}
fn typedef_align(&self, ty: &ast::Type) -> Option<u64> {
match &ty.kind {
ast::TypeKind::Typedef(name) => self.lookup_typedef(name).and_then(|entry| entry.align),
_ => None,
}
}
fn array_of_typedef_align(&self, ty: &ast::Type) -> Option<u64> {
match &ty.kind {
ast::TypeKind::Array { elem, .. } => self
.typedef_align(elem)
.or_else(|| self.array_of_typedef_align(elem)),
_ => None,
}
}
fn size_of(&self, ty: Ty) -> Option<u64> {
self.program.types.size_of(ty, &self.target)
}
fn size_ty(&self) -> Ty {
Ty::size_ty(&self.target)
}
fn empty_list_is_unprototyped(&self) -> bool {
self.gating.standard < crate::Standard::C23
}
fn is_prototyped(&self, func: &ast::FunctionType) -> bool {
func.has_prototype || !self.empty_list_is_unprototyped()
}
fn compatible(&self, a: Ty, b: Ty) -> bool {
self.compatible_in(a, b, &mut Vec::new())
}
fn compatible_in(&self, a: Ty, b: Ty, comparing: &mut Vec<(RecordId, RecordId)>) -> bool {
if a == b {
return true;
}
match (a, b) {
(Ty::Func(x), Ty::Func(y)) => self.func_compatible(x, y, comparing),
(Ty::Pointer(x), Ty::Pointer(y)) => {
let (x, y) = (self.types().pointer_type(x), self.types().pointer_type(y));
x.konst == y.konst && self.compatible_in(x.pointee, y.pointee, comparing)
}
(Ty::Array(x), Ty::Array(y)) => {
let (x, y) = (self.types().array_type(x), self.types().array_type(y));
x.elem_const == y.elem_const
&& x.vla == y.vla
&& self.compatible_in(x.elem, y.elem, comparing)
&& (x.incomplete || y.incomplete || x.len == y.len)
}
(Ty::Record(x), Ty::Record(y)) => self.records_compatible(x, y, comparing),
_ => false,
}
}
fn records_compatible(
&self,
x: RecordId,
y: RecordId,
comparing: &mut Vec<(RecordId, RecordId)>,
) -> bool {
if self.gating.standard < crate::Standard::C23 {
return false;
}
let types = self.types();
let (a, b) = (types.record(x), types.record(y));
let (Some(tag), Some(other)) = (&a.tag, &b.tag) else {
return false;
};
if tag != other || a.kind != b.kind || !a.complete || !b.complete {
return false;
}
self.record_difference(x, y, comparing).is_none()
}
fn func_compatible(
&self,
x: ir::FuncTyId,
y: ir::FuncTyId,
comparing: &mut Vec<(RecordId, RecordId)>,
) -> bool {
let (x, y) = (self.types().func_type(x), self.types().func_type(y));
if !self.compatible_in(x.ret, y.ret, comparing) {
return false;
}
match (x.prototyped, y.prototyped) {
(false, false) => true,
(true, true) => {
x.variadic == y.variadic
&& x.params.len() == y.params.len()
&& x.params
.iter()
.zip(&y.params)
.all(|(a, b)| self.compatible_in(*a, *b, comparing))
}
_ => {
let prototyped = if x.prototyped { x } else { y };
!prototyped.variadic
&& prototyped
.params
.iter()
.all(|p| *p == p.promote_argument(&self.target))
}
}
}
fn composite_signature(&self, a: &Signature, b: &Signature) -> Option<Signature> {
if a == b {
return Some(a.clone());
}
if !self.compatible(a.ret, b.ret) {
return None;
}
match (a.prototyped, b.prototyped) {
(false, false) => Some(a.clone()),
(true, true) => {
let same = a.variadic == b.variadic
&& a.params.len() == b.params.len()
&& a.params
.iter()
.zip(&b.params)
.all(|(x, y)| self.compatible(*x, *y));
same.then(|| a.clone())
}
_ => {
let prototyped = if a.prototyped { a } else { b };
let ok = !prototyped.variadic
&& prototyped
.params
.iter()
.all(|p| *p == p.promote_argument(&self.target));
ok.then(|| prototyped.clone())
}
}
}
fn bit_field_of(&self, place: &Place) -> Option<&ir::BitField> {
let ir::PlaceKind::Field { record, index, .. } = &place.kind else {
return None;
};
self.types().record(*record).fields[*index].bits.as_ref()
}
pub(super) fn is_bit_field_load(&self, expr: &Expr) -> bool {
matches!(&expr.kind, ExprKind::Load(place) if self.bit_field_of(place).is_some())
}
fn promoted(&self, expr: &Expr) -> Ty {
if let ExprKind::Load(place) = &expr.kind
&& let Some(bits) = self.bit_field_of(place)
{
return expr
.ty
.promote_bit_field(bits.width, bits.signed, &self.target);
}
expr.ty.promote(&self.target)
}
fn narrow_bits(&self, expr: &Expr) -> Option<u32> {
if let Some(bits) = expr.bits {
return Some(bits);
}
let ExprKind::Load(place) = &expr.kind else {
return None;
};
let field = self.bit_field_of(place)?;
let promoted = expr
.ty
.promote_bit_field(field.width, field.signed, &self.target);
(promoted == expr.ty && field.width < expr.ty.bits(&self.target)).then_some(field.width)
}
fn operand_bits(&self, expr: &Expr) -> u32 {
if let Some(bits) = self.narrow_bits(expr) {
return bits;
}
let ty = self.promoted(expr);
if ty.is_integer() {
ty.bits(&self.target)
} else {
0
}
}
fn result_bits(&self, common: Ty, operands: [&Expr; 2]) -> Option<u32> {
if !common.is_integer() || !operands.iter().any(|e| self.narrow_bits(e).is_some()) {
return None;
}
let bits = operands.map(|e| self.operand_bits(e)).into_iter().max()?;
(bits < common.bits(&self.target)).then_some(bits)
}
fn promoted_place(&self, place: &Place) -> Ty {
match self.bit_field_of(place) {
Some(bits) => place
.ty
.promote_bit_field(bits.width, bits.signed, &self.target),
None => self.types().unatomic(place.ty).promote(&self.target),
}
}
fn promoted_argument(&self, expr: &Expr) -> Ty {
if expr.ty == Ty::Float {
return Ty::Double;
}
self.promoted(expr)
}
fn new_object(
&mut self,
name: &str,
ty: Ty,
storage: Storage,
is_const: bool,
range: SourceRange,
) -> ObjectId {
let level = self.nest.len().saturating_sub(1) as u32;
self.new_object_at(name, ty, storage, is_const, range, level)
}
fn new_object_at(
&mut self,
name: &str,
ty: Ty,
storage: Storage,
is_const: bool,
range: SourceRange,
level: u32,
) -> ObjectId {
let id = ObjectId(self.program.objects.len() as u32);
self.program.objects.push(ir::Object {
name: name.to_owned(),
ty,
storage,
is_const,
is_register: false,
vla_storage: false,
align: None,
flexible_len: None,
asm_label: None,
section: None,
range,
});
self.object_level.push(level);
id
}
fn object_level(&self, id: ObjectId) -> usize {
self.object_level
.get(id.0 as usize)
.copied()
.unwrap_or_default() as usize
}
fn object_place(&mut self, id: ObjectId, range: SourceRange) -> Place {
let is_const = self.program.object(id).is_const;
let ty = self.visible_object_ty(id);
let Some(ptr) = self.capture(id, range) else {
return place_of(PlaceKind::Object(id), ty, is_const, range);
};
let pty = self.program.object(ptr).ty;
let load = Expr::new(
ExprKind::Load(place_of(PlaceKind::Object(ptr), pty, true, range)),
pty,
range,
);
place_of(PlaceKind::Deref(Box::new(load)), ty, is_const, range)
}
fn capture(&mut self, owner: ObjectId, range: SourceRange) -> Option<ObjectId> {
if self.nest.len() < 2 || self.program.object(owner).storage != Storage::Automatic {
return None;
}
let level = self.object_level(owner);
if level + 1 >= self.nest.len() {
return None;
}
if !self.capturable(owner, range) {
return None;
}
let chain: Vec<FuncId> = self.nest.iter().map(|frame| frame.func).collect();
self.env_chain(&chain, owner, level)
}
fn capturable(&mut self, owner: ObjectId, range: SourceRange) -> bool {
let object = self.program.object(owner);
let (ty, name, declared) = (object.ty, object.name.clone(), object.range);
let reason = if object.vla_storage || self.types().is_vm(ty) {
"it is a variable length array, whose length lives in the enclosing frame, and \
cinrs does not capture that yet. Pass the array and its length as parameters"
} else if ty.is_va_list() {
"a 'va_list' belongs to the function whose arguments it walks, and cinrs does \
not capture one yet. Read the arguments in the enclosing function and pass the \
values"
} else {
return true;
};
let here = self.nest.last().expect("a capture needs a nesting").func;
if self.refused_captures.insert((here, owner)) {
self.error_note(
range,
format!("a nested function cannot use '{name}': {reason}"),
declared,
format!("'{name}' is declared here"),
);
}
false
}
fn env_chain(&mut self, chain: &[FuncId], owner: ObjectId, level: usize) -> Option<ObjectId> {
let mut ptr = None;
for (index, func) in chain.iter().enumerate().skip(level + 1) {
ptr = Some(self.env_param(*func, owner, index));
}
ptr
}
fn env_param(&mut self, func: FuncId, owner: ObjectId, level: usize) -> ObjectId {
if let Some(entry) = self
.program
.function(func)
.env
.iter()
.find(|entry| entry.owner == owner)
{
return entry.param;
}
let object = self.program.object(owner);
let (ty, is_const, name, range) = (
object.ty,
object.is_const,
object.name.clone(),
object.range,
);
let pointer = self.ptr_to(ty, is_const);
let param = self.new_object_at(
&format!("__env_{name}"),
pointer,
Storage::Automatic,
true,
range,
level as u32,
);
self.program.functions[func.0 as usize]
.env
.push(ir::EnvParam { owner, param });
param
}
fn propagate_nested_env(&mut self) {
if self.nested_calls.is_empty() {
return;
}
let edges = std::mem::take(&mut self.nested_calls);
loop {
let mut changed = false;
for (caller, callee) in &edges {
let owners: Vec<ObjectId> = self
.program
.function(*callee)
.env
.iter()
.map(|entry| entry.owner)
.collect();
let Some(chain) = self.nest_chains.get(caller).cloned() else {
continue;
};
for owner in owners {
let level = self.object_level(owner);
if level + 1 >= chain.len() {
continue;
}
let before = self.program.function(*caller).env.len();
self.env_chain(&chain, owner, level);
changed |= self.program.function(*caller).env.len() != before;
}
}
if !changed {
return;
}
}
}
fn check_nested_addresses(&mut self) {
for (id, range) in std::mem::take(&mut self.nested_addresses) {
let function = self.program.function(id);
if function.env.is_empty() {
continue;
}
let name = function.name.clone();
let uses: Vec<String> = function
.env
.iter()
.map(|entry| format!("'{}'", self.program.object(entry.owner).name))
.collect();
let declared = function.range;
self.error_note(
range,
format!(
"the address of the nested function '{name}' cannot be taken: it uses the \
enclosing function's {}, which GCC reaches through a trampoline written \
onto the stack and cinrs cannot generate. Move '{name}' to file scope and \
pass what it uses as parameters",
join_names(&uses)
),
declared,
format!("'{name}' is defined here"),
);
}
}
fn check_nested_definitions(&mut self) {
let missing: Vec<(String, SourceRange)> = self
.program
.functions
.iter()
.filter(|function| function.is_nested() && function.is_extern())
.map(|function| (function.name.clone(), function.range))
.collect();
for (name, range) in missing {
self.error(
range,
format!(
"the nested function '{name}' is declared but never defined; a nested \
function has no linkage, so nothing outside this function can define it"
),
);
}
}
fn check_redefinition(&mut self, name: &ast::Ident) -> bool {
let Some(existing) = self.declared_here(&name.name) else {
return false;
};
let what = existing.describe();
let message = match existing {
Entry::Object(_) => format!("redefinition of '{}'", name.name),
_ => format!("redefinition of '{}' as {what}", name.name),
};
let previous = match existing {
Entry::Object(id) => self.program.object(*id).range,
Entry::Function(id) => self.program.function(*id).range,
Entry::Typedef(entry) => entry.range,
Entry::Constant { range, .. } => *range,
};
self.error_note(
name.range,
message,
previous,
format!("previous declaration of '{}' is", name.name),
);
true
}
fn try_reserve_item_name(&mut self, base: &str) -> bool {
self.item_names.insert(base.to_owned())
}
fn reserve_item_name(&mut self, base: &str) -> String {
if self.try_reserve_item_name(base) {
return base.to_owned();
}
for n in 2u32.. {
let candidate = format!("{base}_{n}");
if self.try_reserve_item_name(&candidate) {
return candidate;
}
}
unreachable!("the loop above always terminates")
}
fn anonymous_name(&mut self, what: &str) -> String {
let n = self.next_anon;
self.next_anon += 1;
let base = format!("__cinrs_{:08x}_{what}{n}", self.program.unit_id as u32);
self.reserve_item_name(&base)
}
fn zero(&mut self, ty: Ty, range: SourceRange) -> Expr {
match ty {
Ty::Atomic(id) => {
let inner = self.types().atomic_inner(id);
self.zero(inner, range)
}
t if t.is_floating() => Expr::new(ExprKind::Float(0.0), ty, range),
t if t.is_integer() => Expr::int(0, ty, range),
Ty::Array(id) => {
let array = self.types().array_type(id);
let elem = self.zero(array.elem, range);
Expr::new(
ExprKind::ArrayRepeat {
value: Box::new(elem),
len: array.len,
},
ty,
range,
)
}
_ => Expr::new(ExprKind::Zeroed, ty, range),
}
}
fn const_to_expr(&self, value: ConstValue, ty: Ty, range: SourceRange) -> Expr {
match value {
ConstValue::Int(v) => Expr::int(v, ty, range),
ConstValue::Float(v) => Expr::new(ExprKind::Float(v), ty, range),
ConstValue::Complex(re, im) => {
let component = ty.complex_component();
let part = |v: f64| Box::new(Expr::new(ExprKind::Float(v), component, range));
Expr::new(
ExprKind::ComplexOf {
re: part(re),
im: part(im),
},
ty,
range,
)
}
}
}
fn const_eval_at(&mut self, expr: &Expr, what: &str) -> Option<ConstValue> {
match self.const_eval(expr) {
Some(value) => Some(value),
None => {
self.error(
expr.range,
format!("{what} is not a compile-time constant expression"),
);
None
}
}
}
fn constant_truth(&mut self, cond: &Expr) -> Option<bool> {
if !cond.ty.is_integer() {
return None;
}
let saved = std::mem::take(&mut self.diags);
let value = self.const_eval(cond);
self.diags = saved;
match value {
Some(ConstValue::Int(v)) => Some(v != 0),
_ => None,
}
}
fn dead_if<T>(&mut self, dead: bool, analyse: impl FnOnce(&mut Self) -> T) -> T {
self.dead_code += u32::from(dead);
let result = analyse(self);
self.dead_code -= u32::from(dead);
result
}
fn const_eval(&mut self, expr: &Expr) -> Option<ConstValue> {
let target = self.target;
match &expr.kind {
ExprKind::Int(v) => Some(ConstValue::Int(*v)),
ExprKind::Float(v) => Some(ConstValue::Float(*v)),
ExprKind::ComplexOf { re, im } => {
let (re, _) = as_parts(self.const_eval(re)?);
let (im, _) = as_parts(self.const_eval(im)?);
Some(complex_value(expr.ty, (re, im)))
}
ExprKind::Load(Place {
kind: PlaceKind::ComplexPart { base, imag },
..
}) => {
let PlaceKind::Temporary(inner) = &base.kind else {
return None;
};
let (re, im) = as_parts(self.const_eval(inner)?);
let part = if *imag { im } else { re };
Some(ConstValue::Float(round_to(part, expr.ty)))
}
ExprKind::Zeroed if expr.ty.is_complex() => Some(ConstValue::Complex(0.0, 0.0)),
ExprKind::Zeroed if expr.ty.is_arithmetic() => Some(if expr.ty.is_floating() {
ConstValue::Float(0.0)
} else {
ConstValue::Int(0)
}),
ExprKind::Cast(inner) => {
if expr.ty.is_integer()
&& inner.ty.is_pointer()
&& let Some(address) = self.integer_pointer_value(inner)
{
return Some(ConstValue::Int(expr.ty.wrap(address, &target)));
}
let value = self.const_eval(inner)?;
expr.ty
.is_arithmetic()
.then(|| convert_const(value, inner.ty, expr.ty, &target))
}
ExprKind::Neg(inner) => match self.const_eval(inner)? {
ConstValue::Int(v) => {
Some(ConstValue::Int(expr.ty.wrap(v.wrapping_neg(), &target)))
}
ConstValue::Float(v) => Some(ConstValue::Float(-v)),
ConstValue::Complex(re, im) => Some(ConstValue::Complex(-re, -im)),
},
ExprKind::BitNot(inner) => match self.const_eval(inner)? {
ConstValue::Int(v) => Some(ConstValue::Int(expr.ty.wrap(!v, &target))),
ConstValue::Float(_) => None,
ConstValue::Complex(re, im) => Some(ConstValue::Complex(re, -im)),
},
ExprKind::Binary { op, lhs, rhs } => {
let lhs = self.const_eval(lhs)?;
let rhs = self.const_eval(rhs)?;
self.const_binary(*op, lhs, rhs, expr)
}
ExprKind::Compare { op, lhs, rhs } => {
let operand_ty = lhs.ty;
let lhs = self.const_eval(lhs)?;
let rhs = self.const_eval(rhs)?;
let result = match (lhs, rhs) {
(ConstValue::Int(a), ConstValue::Int(b)) if unsigned_128(operand_ty) => {
compare_values(*op, &(a as u128), &(b as u128))
}
(ConstValue::Int(a), ConstValue::Int(b)) => compare_values(*op, &a, &b),
(ConstValue::Float(a), ConstValue::Float(b)) => compare_values(*op, &a, &b),
(ConstValue::Complex(ar, ai), ConstValue::Complex(br, bi)) => {
let equal = ar == br && ai == bi;
match op {
ir::CmpOp::Eq => equal,
ir::CmpOp::Ne => !equal,
_ => return None,
}
}
_ => return None,
};
Some(ConstValue::Int(i128::from(result)))
}
ExprKind::Logical { op, lhs, rhs } => {
let lhs = is_true(self.const_eval(lhs)?);
let result = match (op, lhs) {
(ir::LogicalOp::And, false) => false,
(ir::LogicalOp::Or, true) => true,
_ => is_true(self.const_eval(rhs)?),
};
Some(ConstValue::Int(i128::from(result)))
}
ExprKind::Cond {
cond,
then_expr,
else_expr,
} => {
if is_true(self.const_eval(cond)?) {
self.const_eval(then_expr)
} else {
self.const_eval(else_expr)
}
}
ExprKind::Builtin { op, args } => self.const_bit_builtin(*op, args, expr),
_ => None,
}
}
fn const_bit_builtin(
&mut self,
op: ir::BuiltinOp,
args: &[Expr],
expr: &Expr,
) -> Option<ConstValue> {
use ir::BuiltinOp;
let [arg] = args else {
return None;
};
let bits = u32::try_from(arg.ty.size_bytes(&self.target) * 8).ok()?;
if bits == 0 || bits > 128 {
return None;
}
let ConstValue::Int(value) = self.const_eval(arg)? else {
return None;
};
let mask = if bits == 128 {
u128::MAX
} else {
(1u128 << bits) - 1
};
let bitpattern = (value as u128) & mask;
let result = match op {
BuiltinOp::Popcount => i128::from(bitpattern.count_ones()),
BuiltinOp::Parity => i128::from(bitpattern.count_ones() % 2),
BuiltinOp::Ffs => match bitpattern {
0 => 0,
_ => i128::from(bitpattern.trailing_zeros() + 1),
},
BuiltinOp::Clz if bitpattern != 0 => {
i128::from(bitpattern.leading_zeros() - (128 - bits))
}
BuiltinOp::Ctz if bitpattern != 0 => i128::from(bitpattern.trailing_zeros()),
BuiltinOp::Clrsb => {
let top = bitpattern >> (bits - 1);
let folded = if top == 1 {
!bitpattern & mask
} else {
bitpattern
};
i128::from(if folded == 0 {
bits - 1
} else {
folded.leading_zeros() - (128 - bits) - 1
})
}
BuiltinOp::Bswap => {
let bytes = bits / 8;
let mut swapped = 0u128;
for byte in 0..bytes {
swapped |= ((bitpattern >> (byte * 8)) & 0xff) << ((bytes - 1 - byte) * 8);
}
swapped as i128
}
_ => return None,
};
Some(ConstValue::Int(expr.ty.wrap(result, &self.target)))
}
fn const_binary(
&mut self,
op: ir::BinOp,
lhs: ConstValue,
rhs: ConstValue,
expr: &Expr,
) -> Option<ConstValue> {
use ir::BinOp;
let target = self.target;
if expr.ty.is_complex() {
return self.const_complex_binary(op, lhs, rhs, expr);
}
if let (ConstValue::Float(a), ConstValue::Float(b)) = (lhs, rhs) {
let value = match op {
BinOp::Add => a + b,
BinOp::Sub => a - b,
BinOp::Mul => a * b,
BinOp::Div => a / b,
_ => return None,
};
return Some(ConstValue::Float(round_to(value, expr.ty)));
}
let (ConstValue::Int(a), ConstValue::Int(b)) = (lhs, rhs) else {
return None;
};
let value = match op {
BinOp::Add => a.wrapping_add(b),
BinOp::Sub => a.wrapping_sub(b),
BinOp::Mul => a.wrapping_mul(b),
BinOp::Div | BinOp::Rem => {
if b == 0 {
self.error(
expr.range,
"division by zero in a constant expression".to_owned(),
);
return None;
}
if unsigned_128(expr.ty) {
let (a, b) = (a as u128, b as u128);
let value = if op == BinOp::Div { a / b } else { a % b };
return Some(ConstValue::Int(value as i128));
}
if op == BinOp::Div {
a.wrapping_div(b)
} else {
a.wrapping_rem(b)
}
}
BinOp::BitAnd => a & b,
BinOp::BitXor => a ^ b,
BinOp::BitOr => a | b,
BinOp::Shl | BinOp::Shr => {
let width = i128::from(expr.ty.bits(&target));
if b < 0 || b >= width {
self.error(
expr.range,
format!(
"shift count {b} is out of range for type '{}'",
self.tyname(expr.ty)
),
);
return None;
}
if op == BinOp::Shr && unsigned_128(expr.ty) {
return Some(ConstValue::Int(((a as u128) >> b) as i128));
}
if op == BinOp::Shl { a << b } else { a >> b }
}
};
Some(ConstValue::Int(expr.ty.wrap(value, &target)))
}
fn const_complex_binary(
&mut self,
op: ir::BinOp,
lhs: ConstValue,
rhs: ConstValue,
expr: &Expr,
) -> Option<ConstValue> {
use ir::BinOp;
let ty = expr.ty;
let narrow = ty == Ty::ComplexFloat;
let (a, b) = as_parts(lhs);
let (c, d) = as_parts(rhs);
let left_complex = matches!(lhs, ConstValue::Complex(..));
let right_complex = matches!(rhs, ConstValue::Complex(..));
let parts = match op {
BinOp::Add => match (left_complex, right_complex) {
(true, true) => (a + c, b + d),
(true, false) => (a + c, b),
(false, _) => (a + c, d),
},
BinOp::Sub => match (left_complex, right_complex) {
(true, true) => (a - c, b - d),
(true, false) => (a - c, b),
(false, _) => (a - c, -d),
},
BinOp::Mul => {
if left_complex && right_complex {
if narrow {
crate::complex::mul_f32((a, b), (c, d))
} else {
crate::complex::mul((a, b), (c, d))
}
} else if left_complex {
(a * c, b * c)
} else {
(a * c, a * d)
}
}
BinOp::Div => {
if right_complex {
if narrow {
crate::complex::div_f32((a, b), (c, d))
} else {
crate::complex::div((a, b), (c, d))
}
} else {
(a / c, b / c)
}
}
_ => return None,
};
Some(complex_value(ty, parts))
}
}
const VA_LIST_PLACEMENT: &str = "va_list is only supported as a local variable or parameter";
const JUMP_INTO_VM_SCOPE: &str = "jump into the scope of an identifier with variably modified type";
enum ConvContext {
Assign,
Init(String),
Return,
Argument { index: usize, func: String },
}
impl ConvContext {
fn message(&self, from: &str, to: &str) -> String {
match self {
ConvContext::Assign => {
format!("assigning to '{to}' from incompatible type '{from}'")
}
ConvContext::Init(name) => format!(
"cannot initialize '{name}', of type '{to}', with an expression of type '{from}'"
),
ConvContext::Return => {
format!("returning '{from}' from a function with incompatible result type '{to}'")
}
ConvContext::Argument { index, func } => format!(
"passing '{from}' to parameter {index} of '{func}', of incompatible type '{to}'"
),
}
}
}
fn compare_op(op: ast::BinaryOp) -> Option<ir::CmpOp> {
use ir::CmpOp;
Some(match op {
ast::BinaryOp::Lt => CmpOp::Lt,
ast::BinaryOp::Gt => CmpOp::Gt,
ast::BinaryOp::Le => CmpOp::Le,
ast::BinaryOp::Ge => CmpOp::Ge,
ast::BinaryOp::Eq => CmpOp::Eq,
ast::BinaryOp::Ne => CmpOp::Ne,
_ => return None,
})
}
fn arith_op(op: ast::BinaryOp) -> Option<ir::BinOp> {
use ir::BinOp;
Some(match op {
ast::BinaryOp::Add => BinOp::Add,
ast::BinaryOp::Sub => BinOp::Sub,
ast::BinaryOp::Mul => BinOp::Mul,
ast::BinaryOp::Div => BinOp::Div,
ast::BinaryOp::Rem => BinOp::Rem,
ast::BinaryOp::Shl => BinOp::Shl,
ast::BinaryOp::Shr => BinOp::Shr,
ast::BinaryOp::BitAnd => BinOp::BitAnd,
ast::BinaryOp::BitXor => BinOp::BitXor,
ast::BinaryOp::BitOr => BinOp::BitOr,
_ => return None,
})
}
fn compare_values<T: PartialOrd>(op: ir::CmpOp, a: &T, b: &T) -> bool {
use ir::CmpOp;
match op {
CmpOp::Lt => a < b,
CmpOp::Gt => a > b,
CmpOp::Le => a <= b,
CmpOp::Ge => a >= b,
CmpOp::Eq => a == b,
CmpOp::Ne => a != b,
}
}
fn unsigned_128(ty: Ty) -> bool {
ty == Ty::UInt128
}
fn is_true(value: ConstValue) -> bool {
match value {
ConstValue::Int(v) => v != 0,
ConstValue::Float(v) => v != 0.0,
ConstValue::Complex(re, im) => re != 0.0 || im != 0.0,
}
}
fn as_parts(value: ConstValue) -> crate::complex::Parts {
match value {
ConstValue::Int(v) => (v as f64, 0.0),
ConstValue::Float(v) => (v, 0.0),
ConstValue::Complex(re, im) => (re, im),
}
}
fn complex_value(ty: Ty, (re, im): crate::complex::Parts) -> ConstValue {
let component = ty.complex_component();
ConstValue::Complex(round_to(re, component), round_to(im, component))
}
fn convert_const(value: ConstValue, from: Ty, to: Ty, target: &TargetModel) -> ConstValue {
if to.is_complex() {
return complex_value(to, as_parts(value));
}
if let ConstValue::Complex(re, _) = value {
return convert_const(ConstValue::Float(re), from.complex_component(), to, target);
}
match (value, to.is_floating()) {
(ConstValue::Complex(..), _) => value,
(ConstValue::Int(v), false) => ConstValue::Int(to.wrap(v, target)),
(ConstValue::Int(v), true) if unsigned_128(from) => {
ConstValue::Float(round_to(v as u128 as f64, to))
}
(ConstValue::Int(v), true) => ConstValue::Float(round_to(v as f64, to)),
(ConstValue::Float(v), true) => ConstValue::Float(round_to(v, to)),
(ConstValue::Float(v), false) => {
if to.is_bool() {
ConstValue::Int(i128::from(v != 0.0))
} else if unsigned_128(to) {
ConstValue::Int(v.trunc() as u128 as i128)
} else {
ConstValue::Int(to.wrap(v.trunc() as i128, target))
}
}
}
}
fn round_to(value: f64, ty: Ty) -> f64 {
if ty != Ty::Float {
return value;
}
if value.is_nan() {
return f64::from_bits(ir::widen_nan_bits(ir::narrow_nan_bits(value.to_bits())));
}
value as f32 as f64
}
fn render_case_value(value: i128, ty: Ty, target: &TargetModel) -> String {
if ty.is_signed(target) {
value.to_string()
} else {
(value as u128).to_string()
}
}
fn join_names(names: &[String]) -> String {
match names {
[] => String::new(),
[one] => one.clone(),
[rest @ .., last] => format!("{} and {last}", rest.join(", ")),
}
}
fn place_of(kind: ir::PlaceKind, ty: Ty, is_const: bool, range: SourceRange) -> Place {
Place {
kind,
ty,
is_const,
range,
}
}