diff --git a/crates/emmylua_code_analysis/src/semantic_model/flow.rs b/crates/emmylua_code_analysis/src/semantic_model/flow.rs index c76b063e6..65b550480 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/flow.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/flow.rs @@ -16,11 +16,13 @@ use std::collections::HashSet; use std::rc::Rc; use emmylua_parser::{ - BinaryOperator, LuaAssignStat, LuaAstNode, LuaCallExprStat, LuaDocTagAs, LuaExpr, + BinaryOperator, LuaAssignStat, LuaAstNode, LuaCallExprStat, LuaComment, LuaExpr, LuaLiteralToken, UnaryOperator, }; use rowan::TextSize; +use ExprCastMode::{All, AsOnly}; + use crate::semantic_db::def::{SemanticId, Signature}; use crate::semantic_db::flow::{FlowAntecedent, FlowEffect, FlowId, FlowNodeKind, FlowTree}; use crate::{ @@ -440,9 +442,14 @@ pub fn type_of_expr_at( match &expr { LuaExpr::NameExpr(name_expr) => { if let Some(decl) = model.resolve_name(name_expr.get_position()) { - return model.type_of_decl_at(&decl, offset); + return apply_expr_casts( + model, + expr_syntax, + model.type_of_decl_at(&decl, offset), + AsOnly, + ); } - model.type_of_expr(expr_syntax) + type_of_expr_with_cast(model, expr_syntax, AsOnly) } LuaExpr::IndexExpr(index_expr) => { if let Some(resolved) = model.resolve_member(index_expr) @@ -493,7 +500,7 @@ pub fn type_of_expr_at( ) && let Some(prefix_member_ty) = model.member_type(&prefix_ty, &key) && !matches!(prefix_member_ty, LuaType::Unknown) { - return apply_expr_casts(model, expr_syntax, prefix_member_ty); + return apply_expr_casts(model, expr_syntax, prefix_member_ty, All); } } // Computed-key fields in table literals (`[key] = 1`) keep literal values in type queries: @@ -516,16 +523,16 @@ pub fn type_of_expr_at( // Dynamic access to a computed key `[key] = 1` can return `integer`; // named fields still keep `number` through the existing projection. LuaType::IntegerConst(_) | LuaType::DocIntegerConst(_) => { - return apply_expr_casts(model, expr_syntax, LuaType::Integer); + return apply_expr_casts(model, expr_syntax, LuaType::Integer, All); } LuaType::StringConst(_) | LuaType::DocStringConst(_) => { - return apply_expr_casts(model, expr_syntax, expr_ty); + return apply_expr_casts(model, expr_syntax, expr_ty, All); } _ => {} } } if let Some(ty) = model.member_type(&prefix_ty, &member_def.key) { - return apply_expr_casts(model, expr_syntax, ty); + return apply_expr_casts(model, expr_syntax, ty, All); } } if let SemanticId::Decl(owner_decl) = &resolved.owner @@ -541,7 +548,7 @@ pub fn type_of_expr_at( { let runtime_ty = type_of_member_at(model, &runtime_member.id, offset); if !matches!(runtime_ty, LuaType::Unknown) { - return apply_expr_casts(model, expr_syntax, runtime_ty); + return apply_expr_casts(model, expr_syntax, runtime_ty, All); } } } @@ -556,19 +563,19 @@ pub fn type_of_expr_at( && (member_type_more_specific(model, &prefix_member_ty, &member_ty) || matches!(&prefix_ty, LuaType::Generic(_))) { - return apply_expr_casts(model, expr_syntax, prefix_member_ty); + return apply_expr_casts(model, expr_syntax, prefix_member_ty, All); } } - return member_ty; + return apply_expr_casts(model, expr_syntax, member_ty, AsOnly); } // Inside a `#d == n` branch, `d[1]` no longer carries nil when the array length is known. if let Some(known_ty) = array_index_with_known_len(model, &index_expr, offset) { - return apply_expr_casts(model, expr_syntax, known_ty); + return apply_expr_casts(model, expr_syntax, known_ty, All); } - apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax)) + apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax), All) } LuaExpr::CallExpr(_) | LuaExpr::TableExpr(_) | LuaExpr::LiteralExpr(_) => { - apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax)) + apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax), All) } LuaExpr::ClosureExpr(closure_expr) => { // Anonymous closure keeps its actual signature on the flow path, avoiding the assigned function value degrading to bare `Function`. @@ -576,14 +583,14 @@ pub fn type_of_expr_at( .type_of_signature(closure_expr.get_syntax_id()) .map(|fun| LuaType::DocFunction(Rc::new(fun))) .unwrap_or_else(|| model.type_of_expr(expr_syntax)); - apply_expr_casts(model, expr_syntax, base) + apply_expr_casts(model, expr_syntax, base, All) } LuaExpr::BinaryExpr(binary_expr) => { let Some(op_token) = binary_expr.get_op_token() else { - return apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax)); + return apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax), All); }; let Some((left_expr, right_expr)) = binary_expr.get_exprs() else { - return apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax)); + return apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax), All); }; // Preserve the legacy semantics that "array table literals in short-circuit expressions are tuple": // only when the right side is a sequential array table and the left side is a logical short-circuit, replace the right table type with tuple. @@ -594,7 +601,7 @@ pub fn type_of_expr_at( { let left_ty = type_of_expr_at(model, left_expr.get_syntax_id(), offset); let result = logical_binary_type(model, op_token.get_op(), left_ty, tuple_ty); - return apply_expr_casts(model, expr_syntax, result); + return apply_expr_casts(model, expr_syntax, result, All); } // General binary expressions: subexpressions first use flow-sensitive types (member/variable branch narrowing), // then merge with the same VM operator rules, ensuring `right.value + 1` uses the narrow type of `right.value` in the branch. @@ -614,23 +621,23 @@ pub fn type_of_expr_at( ) && (matches!(&left_ty, LuaType::Unknown | LuaType::Any) || matches!(&right_ty, LuaType::Unknown | LuaType::Any)) { - return apply_expr_casts(model, expr_syntax, LuaType::Unknown); + return apply_expr_casts(model, expr_syntax, LuaType::Unknown, All); } let result = binary_type(model, op_token.get_op(), &left_ty, &right_ty); - apply_expr_casts(model, expr_syntax, result) + apply_expr_casts(model, expr_syntax, result, All) } - _ => model.type_of_expr(expr_syntax), + _ => type_of_expr_with_cast(model, expr_syntax, AsOnly), } } -/// Type query that additionally applies inline casts on the expression beyond the flow-sensitive branch. -/// Used for contexts like closure return inference that need "bare VM type + `---@as`", avoiding changing -/// the existing behavior of `type_of_expr_at` for ordinary expressions. +/// Infer the VM type, then apply the selected inline casts. +/// New initializer and assignment reads use `AsOnly` to preserve named-cast scope. pub(crate) fn type_of_expr_with_cast( model: &SemanticModel, expr_syntax: emmylua_parser::LuaSyntaxId, + mode: ExprCastMode, ) -> LuaType { - apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax)) + apply_expr_casts(model, expr_syntax, model.type_of_expr(expr_syntax), mode) } /// Simple array table literal → `LuaType::Tuple` (legacy `{ 'a' }` stays tuple in short-circuit expressions). @@ -1014,12 +1021,20 @@ fn branch_path_reachable( false } +/// Select which cast effects an expression query can apply. +#[derive(Clone, Copy)] +pub(crate) enum ExprCastMode { + All, + AsOnly, +} + /// Apply inline casts bound to this expression (e.g. `A:get(1) --[[@cast -?]]`). /// `+T` widens, `-T` (including `-?`) removes, and no operator replaces. -fn apply_expr_casts( +pub(super) fn apply_expr_casts( model: &SemanticModel, expr_syntax: emmylua_parser::LuaSyntaxId, base: LuaType, + mode: ExprCastMode, ) -> LuaType { let Some(tree) = model.flow_tree() else { return base; @@ -1046,7 +1061,7 @@ fn apply_expr_casts( for flow_id in flow_ids { for effect in tree.get_flow_effects(flow_id) { match effect { - FlowEffect::TagCast(cast_ptr) => { + FlowEffect::TagCast(cast_ptr) if matches!(mode, All) => { let Some(chunk) = model.chunk() else { continue; }; @@ -1084,6 +1099,18 @@ fn apply_expr_casts( let Some(as_tag) = as_ptr.to_node(&chunk) else { continue; }; + let Some(owner) = as_tag + .get_parent::() + .and_then(|comment| comment.get_owner()) + else { + continue; + }; + // Statement comments cast the last value, never its operands or later reads. + let target = LuaExpr::cast(owner.syntax().clone()) + .or_else(|| owner.children::().last()); + if target.is_none_or(|expr| expr.get_syntax_id() != expr_syntax) { + continue; + } if let Some(doc_type) = as_tag.get_type() { ty = model.doc_type_lua(doc_type.get_syntax_id()); } @@ -1465,11 +1492,6 @@ fn trace_decl_uncached( collect_decl_cast(model, decl, cast_ptr, path); } } - FlowNodeKind::AsCast(as_ptr) => { - if options.casts { - collect_decl_as_cast(model, decl, as_ptr, path); - } - } // Backtracked to the start (e.g. function parameters with no DeclPosition node): use the declaration type as the base and apply path guards/casts. FlowNodeKind::Start | FlowNodeKind::Unreachable => { if matches!(node.kind, FlowNodeKind::Unreachable) && mode == TraceMode::MergeBranch @@ -1591,7 +1613,7 @@ fn trace_member( narrowings: Vec::new(), casts: path.casts.clone(), }; - let mut value_ty = model.type_of_expr(*value_syntax); + let mut value_ty = type_of_expr_with_cast(model, *value_syntax, AsOnly); if let Some(annotation) = member_annotation_type(model, member) { if model.type_check(&annotation, &value_ty) { value_ty = widen_const_type(value_ty); @@ -1606,9 +1628,6 @@ fn trace_member( FlowNodeKind::TagCast(cast_ptr) => { collect_member_cast(model, member, cast_ptr, path); } - FlowNodeKind::AsCast(as_ptr) => { - collect_member_as_cast(model, member, as_ptr, path); - } FlowNodeKind::TrueCondition(cond_ptr) => { if let Some(narrowing) = narrow_member_condition(model, member, cond_ptr, true) { path.narrowings.push(narrowing); @@ -1876,46 +1895,6 @@ fn collect_member_cast( } } -/// Inline assertion `--[[@as T]]`: when matching the current decl's path, replace the path type directly with T. -fn collect_decl_as_cast( - model: &SemanticModel, - _decl: &SemanticId, - as_ptr: &emmylua_parser::LuaAstPtr, - path: &mut PathState, -) { - let Some(ty) = as_cast_type(model, as_ptr) else { - return; - }; - if !matches!(ty, LuaType::Unknown) { - path.casts.push(CastOp::Replace(ty)); - } -} - -/// Inline assertion `--[[@as T]]`: when matching the current member's path, replace the path type directly with T. -fn collect_member_as_cast( - model: &SemanticModel, - _member: &SemanticId, - as_ptr: &emmylua_parser::LuaAstPtr, - path: &mut PathState, -) { - let Some(ty) = as_cast_type(model, as_ptr) else { - return; - }; - if !matches!(ty, LuaType::Unknown) { - path.casts.push(CastOp::Replace(ty)); - } -} - -fn as_cast_type( - model: &SemanticModel, - as_ptr: &emmylua_parser::LuaAstPtr, -) -> Option { - let chunk = model.chunk()?; - let as_tag = as_ptr.to_node(&chunk)?; - let doc_type = as_tag.get_type()?; - Some(model.doc_type_lua(doc_type.get_syntax_id())) -} - /// RHS type of an assignment: if decl is a multi-return slot on this assignment, take the matching return type by `return_index`; /// otherwise fall back to ordinary expression inference. fn assigned_value_type( @@ -1972,11 +1951,8 @@ fn assigned_value_type( model.type_of_expr(value_syntax), ); }; - let expanded = - model.infer_expr_list_types(&[LuaExpr::CallExpr(call)], Some(reference.return_index + 1)); - expanded - .get(reference.return_index) - .map(|(ty, _)| ty.clone()) + type_of_expr_with_cast(model, call.get_syntax_id(), AsOnly) + .get_result_slot_type(reference.return_index) .unwrap_or_else(|| { if reference.return_index > 0 { // When a finite-return call is assigned to multiple variables, slots beyond the return count are nil. diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/flow.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/flow.rs index 235a91b5e..e71332e0c 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/flow.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/legacy_tests/flow.rs @@ -10,6 +10,248 @@ mod test { const REPEATED_SELF_ASSIGNMENT_STEPS: usize = 512; const REPEATED_SELF_ASSIGNMENT_VARIANT_STEPS: usize = 128; + #[test] + fn test_inline_as_does_not_apply_named_cast_to_initializers() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file = ws.def( + r#" + ---@param value string + local function consume(value) print(value) end + ---@param count number + ---@param text string + local function run(count, text) + ---@cast count integer + local copy = text + consume(copy) + print(count) + end + run(1, 'text') + "#, + ); + assert_eq!(last_name_expr_type(&ws, file, "copy"), LuaType::String); + assert_eq!(last_name_expr_type(&ws, file, "count"), LuaType::Integer); + let diagnostics = ws + .analysis + .diagnose_file(file, tokio_util::sync::CancellationToken::new()) + .unwrap(); + assert!(diagnostics.is_empty(), "{diagnostics:#?}"); + } + + #[test] + fn test_inline_as_does_not_apply_named_cast_to_table_fields() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file = ws.def( + r#" + ---@param value string + local function consume(value) print(value) end + ---@param count number + ---@param text string + local function run(count, text) + ---@cast count integer + local state = { text = text } + local copy = state.text + consume(copy) + end + run(1, 'text') + "#, + ); + assert_eq!(last_name_expr_type(&ws, file, "copy"), LuaType::String); + let diagnostics = ws + .analysis + .diagnose_file(file, tokio_util::sync::CancellationToken::new()) + .unwrap(); + assert!(diagnostics.is_empty(), "{diagnostics:#?}"); + } + + #[test] + fn test_inline_as_does_not_cast_later_call() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + assert!(ws.has_no_diagnostic( + DiagnosticCode::CallNonCallable, + r#" + local function run() + local value = 1 --[[@as integer]] + print(value) + end + run() + "#, + )); + } + + #[test] + fn test_inline_as_preserves_cast_and_unrelated_types() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file = ws.def( + r#" + ---@param source unknown + local function run(source) + local value = source --[[@as integer]] + local text = tostring(value) + print(text, source) + end + run('input') + "#, + ); + assert_eq!(last_name_expr_type(&ws, file, "value"), LuaType::Integer); + assert_eq!(last_name_expr_type(&ws, file, "text"), LuaType::String); + assert_eq!(last_name_expr_type(&ws, file, "source"), LuaType::Unknown); + } + + #[test] + fn test_inline_as_assignment_cast_stays_with_last_value() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file = ws.def( + r#" + ---@class InlineCastValue + ---@field count integer + local first, second, object + first, second = 'text', {} --[[@as InlineCastValue]] + do + object = {} --[[@as InlineCastValue]] + print(object.count) + end + print(first, second, object) + "#, + ); + assert_eq!(last_name_expr_type(&ws, file, "first"), ws.ty("'text'")); + assert_eq!( + last_name_expr_type(&ws, file, "second"), + ws.ty("InlineCastValue") + ); + assert_eq!( + last_name_expr_type(&ws, file, "object"), + ws.ty("InlineCastValue") + ); + } + + #[test] + fn test_inline_as_member_assignment_preserves_unrelated_members() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file = ws.def( + r#" + ---@class InlineCastMember + ---@field count integer + local state = { text = 'text' } + state.object = {} --[[@as InlineCastMember]] + local object = state.object + local text = state.text + local count = state.text --[[@as integer]] + print(object, text, count) + "#, + ); + assert_eq!( + last_name_expr_type(&ws, file, "object"), + ws.ty("InlineCastMember") + ); + assert_eq!(last_name_expr_type(&ws, file, "text"), ws.ty("'text'")); + assert_eq!(last_name_expr_type(&ws, file, "count"), LuaType::Integer); + } + + #[test] + fn test_inline_as_preserves_initializer_expression_casts() { + let mut ws = VirtualWorkspace::new(); + for expression in ["'input'", "(true)", "function() end", "{}", "1 + 2"] { + assert_eq!( + ws.expr_ty(&format!("{expression} --[[@as integer]]")), + LuaType::Integer, + "{expression}", + ); + } + } + + #[test] + fn test_inline_as_call_assignment_keeps_the_cast_result() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file = ws.def( + r#" + ---@class InlineCastOptions + ---@field count integer + local _InlineCastOptions = {} + ---@param value unknown + ---@return unknown + local function identity(value) return value end + ---@param options? InlineCastOptions + local function run(options) + options = identity(options) --[[@as InlineCastOptions]] + print(options.count) + end + run() + "#, + ); + assert_eq!( + last_name_expr_type(&ws, file, "options"), + ws.ty("InlineCastOptions") + ); + let diagnostics = ws + .analysis + .diagnose_file(file, tokio_util::sync::CancellationToken::new()) + .unwrap(); + assert!(diagnostics.is_empty(), "{diagnostics:#?}"); + } + + #[test] + fn test_inline_as_does_not_cast_call_parts_or_other_arguments() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file = ws.def( + r#" + ---@param value unknown + ---@return unknown + local function identity(value) return value end + ---@param value integer + ---@param text string + local function consume(value, text) print(value, text) end + local value = identity('input') --[[@as integer]] + consume(value, 'text') + consume(identity('input') --[[@as integer]], 'text') + print(value) + "#, + ); + assert_eq!(last_name_expr_type(&ws, file, "value"), LuaType::Integer); + let diagnostics = ws + .analysis + .diagnose_file(file, tokio_util::sync::CancellationToken::new()) + .unwrap(); + assert!(diagnostics.is_empty(), "{diagnostics:#?}"); + } + + #[test] + fn test_inline_as_keeps_invalid_call_and_argument_diagnostics() { + let mut ws = VirtualWorkspace::new_with_init_std_lib(); + let file = ws.def( + r#" + ---@param value string + local function consume(value) print(value) end + local function run() + local value = 'input' --[[@as integer]] + value() + consume(value) + end + run() + "#, + ); + let mut diagnostics = ws + .analysis + .diagnose_file(file, tokio_util::sync::CancellationToken::new()) + .unwrap(); + diagnostics.sort_by_key(|diagnostic| diagnostic.range.start); + let codes: Vec<_> = diagnostics + .iter() + .map(|diagnostic| diagnostic.code.clone()) + .collect(); + assert_eq!( + codes, + vec![ + Some(lsp_types::NumberOrString::String( + "call-non-callable".into() + )), + Some(lsp_types::NumberOrString::String( + "param-type-mismatch".into() + )), + ], + "{diagnostics:#?}" + ); + } + fn last_name_expr_type(ws: &VirtualWorkspace, file_id: FileId, name: &str) -> LuaType { let semantic_model = ws.analysis.semantic_model(file_id); let chunk = semantic_model.chunk().expect("chunk must exist"); diff --git a/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs b/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs index 63e77c54a..dc37066e9 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/infer/vm.rs @@ -22,7 +22,7 @@ use crate::semantic_db::def::{ SignatureDoc, }; use crate::semantic_db::types::PrimitiveType; -use crate::semantic_model::flow::type_of_expr_with_cast; +use crate::semantic_model::flow::{ExprCastMode, type_of_expr_with_cast}; use crate::semantic_model::member::{MemberInfo, table_const_class_type, table_metatable_type}; use crate::semantic_model::type_eval::{ class_generic_map, eval_conditionals, expand_alias_generic, sanitize_unresolved_generics, @@ -2162,7 +2162,7 @@ impl<'a> InferVm<'a> { let mut ty = vm.run(); // `---@as` / flow casts are attached to return expression nodes; flow queries can // give a more precise return type than bare VM (e.g. `{} --[[@as Promise]]`). - let flow_ty = type_of_expr_with_cast(self.model, expr_syntax); + let flow_ty = type_of_expr_with_cast(self.model, expr_syntax, ExprCastMode::All); if !matches!(flow_ty, LuaType::Unknown | LuaType::Any) && flow_ty != ty { ty = flow_ty; } diff --git a/crates/emmylua_code_analysis/src/semantic_model/types.rs b/crates/emmylua_code_analysis/src/semantic_model/types.rs index 42e5cca53..d92d0f3eb 100644 --- a/crates/emmylua_code_analysis/src/semantic_model/types.rs +++ b/crates/emmylua_code_analysis/src/semantic_model/types.rs @@ -220,7 +220,12 @@ impl<'db> SemanticModel<'db> { && let Some(closure) = LuaClosureExpr::cast(node) && let Some(fun) = self.type_of_signature(closure.get_syntax_id()) { - return Some(LuaType::DocFunction(Rc::new(fun))); + return Some(flow::apply_expr_casts( + self, + value_syntax, + LuaType::DocFunction(Rc::new(fun)), + flow::ExprCastMode::AsOnly, + )); } // Initializer expression: VM inference; re-entrant declaration/expression queries // return `None` through `CacheEntry::InProgress`. @@ -234,7 +239,7 @@ impl<'db> SemanticModel<'db> { let ty = if use_flow { self.type_of_expr_at(value_syntax, value_syntax.get_range().start()) } else { - self.type_of_expr(value_syntax) + flow::type_of_expr_with_cast(self, value_syntax, flow::ExprCastMode::AsOnly) }; if !matches!(ty, LuaType::Unknown) { // Multi-return assignment slot: the `b` in `local a, b = f()` takes f()'s 2nd return. @@ -782,9 +787,13 @@ impl<'db> SemanticModel<'db> { let node = value_syntax.to_node_from_root(&tree.get_red_root())?; let expr = LuaExpr::cast(node)?; let eval = if self.is_initializer_table_field(member, &member_def) { - self.type_of_expr(expr.get_syntax_id()) + flow::type_of_expr_with_cast( + self, + expr.get_syntax_id(), + flow::ExprCastMode::AsOnly, + ) } else if matches!(expr, LuaExpr::CallExpr(_)) { - self.type_of_expr(value_syntax) + flow::type_of_expr_with_cast(self, value_syntax, flow::ExprCastMode::AsOnly) } else { return None; }; @@ -802,6 +811,15 @@ impl<'db> SemanticModel<'db> { .type_shell_lua_in(member_file, &shell, &generic_names); let ty = type_eval::expand_alias_generic(self, &ty); let ty = type_eval::eval_conditionals(self, &ty); + let ty = if let Some(facts) = self.file_facts_of(member_file) + && let Some(member_def) = facts.member_by_id(member) + && member_def.doc_type_syntax.is_none() + && let Some(value_syntax) = member_def.value_syntax + { + flow::apply_expr_casts(self, value_syntax, ty, flow::ExprCastMode::AsOnly) + } else { + ty + }; Some(ty) } /// Whether this is a table-literal field in a declaration initializer.