From 134d43539e7c722669bd33a911cd95639c3af6ef Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Wed, 23 Apr 2025 00:19:08 -0600
Subject: [PATCH 01/10] feat: State declarations in class constructors

---
 .../2-analyze/visitors/CallExpression.js      | 100 +++++++++++++++++-
 1 file changed, 97 insertions(+), 3 deletions(-)

diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index 904817b014e4..683e343aa56a 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -116,9 +116,11 @@ export function CallExpression(node, context) {
 		case '$derived':
 		case '$derived.by':
 			if (
-				(parent.type !== 'VariableDeclarator' ||
-					get_parent(context.path, -3).type === 'ConstTag') &&
-				!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
+				!(
+					call_expression_is_variable_declaration(parent, context) ||
+					call_expression_is_class_property_definition(parent) ||
+					call_expression_is_valid_class_property_assignment_in_constructor(parent, context)
+				)
 			) {
 				e.state_invalid_placement(node, rune);
 			}
@@ -270,3 +272,95 @@ function get_function_label(nodes) {
 		return parent.id.name;
 	}
 }
+
+/**
+ *
+ * @param {AST.SvelteNode} parent
+ * @param {Context} context
+ */
+function call_expression_is_variable_declaration(parent, context) {
+	return parent.type === 'VariableDeclarator' && get_parent(context.path, -3).type !== 'ConstTag';
+}
+
+/**
+ *
+ * @param {AST.SvelteNode} parent
+ */
+function call_expression_is_class_property_definition(parent) {
+	return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed;
+}
+
+/**
+ *
+ * @param {AST.SvelteNode} parent
+ * @param {Context} context
+ * @returns
+ */
+function call_expression_is_valid_class_property_assignment_in_constructor(parent, context) {
+	return (
+		expression_is_assignment_to_top_level_property_of_this(parent) &&
+		current_node_is_in_constructor_root_or_control_flow_blocks(context)
+	);
+}
+
+/**
+ * yes:
+ *   - `this.foo = bar`
+ *
+ * no:
+ *   - `this = bar`
+ *   - `this.foo.baz = bar`
+ *   - `anything_other_than_this = bar`
+ *
+ * @param {AST.SvelteNode} node
+ */
+function expression_is_assignment_to_top_level_property_of_this(node) {
+	return (
+		node.type === 'AssignmentExpression' &&
+		node.operator === '=' &&
+		node.left.type === 'MemberExpression' &&
+		node.left.object.type === 'ThisExpression' &&
+		node.left.property.type === 'Identifier'
+	);
+}
+
+/**
+ * @param {AST.SvelteNode} node
+ */
+function node_is_constructor(node) {
+	return (
+		node.type === 'MethodDefinition' &&
+		node.key.type === 'Identifier' &&
+		node.key.name === 'constructor'
+	);
+}
+
+// if blocks are just IfStatements with BlockStatements or other IfStatements as consequents
+const allowed_parent_types = new Set([
+	'IfStatement',
+	'BlockStatement',
+	'SwitchCase',
+	'SwitchStatement'
+]);
+
+/**
+ * Succeeds if the node's only direct parents are `if` / `else if` / `else` blocks _and_
+ * those blocks are the direct children of the constructor.
+ *
+ * @param {Context} context
+ */
+function current_node_is_in_constructor_root_or_control_flow_blocks(context) {
+	let parent_index = -3; // this gets us from CallExpression -> AssignmentExpression -> ExpressionStatement -> Whatever is here
+	while (true) {
+		const grandparent = get_parent(context.path, parent_index - 1);
+		const parent = get_parent(context.path, parent_index);
+		if (grandparent && node_is_constructor(grandparent)) {
+			// if this is the case then `parent` is the FunctionExpression
+			return true;
+		}
+		if (!allowed_parent_types.has(parent.type)) {
+			return false;
+		}
+		parent_index--;
+	}
+}

From fb8d6d7975ff53e833779cb0ac8d3e117f768fb8 Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Thu, 24 Apr 2025 15:02:43 -0600
Subject: [PATCH 02/10] feat: Analysis phase

---
 .../98-reference/.generated/compile-errors.md |  33 +++-
 .../svelte/messages/compile-errors/script.md  |  31 +++-
 packages/svelte/src/compiler/errors.js        |  15 +-
 .../src/compiler/phases/2-analyze/index.js    |  10 +-
 .../src/compiler/phases/2-analyze/types.d.ts  |   5 +-
 .../visitors/AssignmentExpression.js          |   1 +
 .../2-analyze/visitors/CallExpression.js      |  80 +---------
 .../phases/2-analyze/visitors/ClassBody.js    |  30 ----
 .../2-analyze/visitors/ClassDeclaration.js    |   3 +-
 .../2-analyze/visitors/PropertyDefinition.js  |  12 ++
 .../visitors/shared/class-analysis.js         | 147 ++++++++++++++++++
 .../class-state-field-static/_config.js       |   2 +-
 .../samples/runes-no-rune-each/_config.js     |   3 +-
 .../runes-wrong-derived-placement/_config.js  |   2 +-
 .../runes-wrong-state-placement/_config.js    |   3 +-
 .../const-tag-invalid-rune-usage/errors.json  |   2 +-
 16 files changed, 257 insertions(+), 122 deletions(-)
 delete mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
 create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js
 create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js

diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index e8669ead533d..fb0591b884a5 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -208,6 +208,37 @@ Cannot assign to %thing%
 Cannot bind to %thing%
 ```
 
+### constructor_state_reassignment
+
+```
+Cannot redeclare stateful field `%name%` in the constructor. The field was originally declared here: `%original_location%`
+```
+
+To create stateful class fields in the constructor, the rune assignment must be the _first_ assignment to the class field.
+Assignments thereafter must not use the rune.
+
+```ts
+constructor() {
+	this.count = $state(0);
+	this.count = $state(1); // invalid, assigning to the same property with `$state` again
+}
+
+constructor() {
+	this.count = $state(0);
+	this.count = $state.raw(1); // invalid, assigning to the same property with a different rune
+}
+
+constructor() {
+	this.count = 0;
+	this.count = $state(1); // invalid, this property was created as a regular property, not state
+}
+
+constructor() {
+	this.count = $state(0);
+	this.count = 1; // valid, this is setting the state that has already been declared
+}
+```
+
 ### css_empty_declaration
 
 ```
@@ -855,7 +886,7 @@ Cannot export state from a module if it is reassigned. Either export a function
 ### state_invalid_placement
 
 ```
-`%rune%(...)` can only be used as a variable declaration initializer or a class field
+`%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
 ```
 
 ### store_invalid_scoped_subscription
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index aabcbeae4812..780c9f4d2863 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -10,6 +10,35 @@
 
 > Cannot bind to %thing%
 
+## constructor_state_reassignment
+
+> Cannot redeclare stateful field `%name%` in the constructor. The field was originally declared here: `%original_location%`
+
+To create stateful class fields in the constructor, the rune assignment must be the _first_ assignment to the class field.
+Assignments thereafter must not use the rune.
+
+```ts
+constructor() {
+	this.count = $state(0);
+	this.count = $state(1); // invalid, assigning to the same property with `$state` again
+}
+
+constructor() {
+	this.count = $state(0);
+	this.count = $state.raw(1); // invalid, assigning to the same property with a different rune
+}
+
+constructor() {
+	this.count = 0;
+	this.count = $state(1); // invalid, this property was created as a regular property, not state
+}
+
+constructor() {
+	this.count = $state(0);
+	this.count = 1; // valid, this is setting the state that has already been declared
+}
+```
+
 ## declaration_duplicate
 
 > `%name%` has already been declared
@@ -218,7 +247,7 @@ It's possible to export a snippet from a `<script module>` block, but only if it
 
 ## state_invalid_placement
 
-> `%rune%(...)` can only be used as a variable declaration initializer or a class field
+> `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
 
 ## store_invalid_scoped_subscription
 
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index c99f59746863..fc814a16d31a 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -104,6 +104,17 @@ export function constant_binding(node, thing) {
 	e(node, 'constant_binding', `Cannot bind to ${thing}\nhttps://svelte.dev/e/constant_binding`);
 }
 
+/**
+ * Cannot redeclare stateful field `%name%` in the constructor. The field was originally declared here: `%original_location%`
+ * @param {null | number | NodeLike} node
+ * @param {string} name
+ * @param {string} original_location
+ * @returns {never}
+ */
+export function constructor_state_reassignment(node, name, original_location) {
+	e(node, 'constructor_state_reassignment', `Cannot redeclare stateful field \`${name}\` in the constructor. The field was originally declared here: \`${original_location}\`\nhttps://svelte.dev/e/constructor_state_reassignment`);
+}
+
 /**
  * `%name%` has already been declared
  * @param {null | number | NodeLike} node
@@ -471,13 +482,13 @@ export function state_invalid_export(node) {
 }
 
 /**
- * `%rune%(...)` can only be used as a variable declaration initializer or a class field
+ * `%rune%(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.
  * @param {null | number | NodeLike} node
  * @param {string} rune
  * @returns {never}
  */
 export function state_invalid_placement(node, rune) {
-	e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer or a class field\nhttps://svelte.dev/e/state_invalid_placement`);
+	e(node, 'state_invalid_placement', `\`${rune}(...)\` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.\nhttps://svelte.dev/e/state_invalid_placement`);
 }
 
 /**
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 2e36a896493f..ac9ab7689b41 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -22,7 +22,6 @@ import { Attribute } from './visitors/Attribute.js';
 import { AwaitBlock } from './visitors/AwaitBlock.js';
 import { BindDirective } from './visitors/BindDirective.js';
 import { CallExpression } from './visitors/CallExpression.js';
-import { ClassBody } from './visitors/ClassBody.js';
 import { ClassDeclaration } from './visitors/ClassDeclaration.js';
 import { ClassDirective } from './visitors/ClassDirective.js';
 import { Component } from './visitors/Component.js';
@@ -46,6 +45,7 @@ import { LetDirective } from './visitors/LetDirective.js';
 import { MemberExpression } from './visitors/MemberExpression.js';
 import { NewExpression } from './visitors/NewExpression.js';
 import { OnDirective } from './visitors/OnDirective.js';
+import { PropertyDefinition } from './visitors/PropertyDefinition.js';
 import { RegularElement } from './visitors/RegularElement.js';
 import { RenderTag } from './visitors/RenderTag.js';
 import { SlotElement } from './visitors/SlotElement.js';
@@ -135,7 +135,6 @@ const visitors = {
 	AwaitBlock,
 	BindDirective,
 	CallExpression,
-	ClassBody,
 	ClassDeclaration,
 	ClassDirective,
 	Component,
@@ -159,6 +158,7 @@ const visitors = {
 	MemberExpression,
 	NewExpression,
 	OnDirective,
+	PropertyDefinition,
 	RegularElement,
 	RenderTag,
 	SlotElement,
@@ -259,7 +259,7 @@ export function analyze_module(ast, options) {
 			scope,
 			scopes,
 			analysis: /** @type {ComponentAnalysis} */ (analysis),
-			derived_state: [],
+			class_state: null,
 			// TODO the following are not needed for modules, but we have to pass them in order to avoid type error,
 			// and reducing the type would result in a lot of tedious type casts elsewhere - find a good solution one day
 			ast_type: /** @type {any} */ (null),
@@ -618,7 +618,7 @@ export function analyze_component(root, source, options) {
 				has_props_rune: false,
 				component_slots: new Set(),
 				expression: null,
-				derived_state: [],
+				class_state: null,
 				function_depth: scope.function_depth,
 				reactive_statement: null
 			};
@@ -685,7 +685,7 @@ export function analyze_component(root, source, options) {
 				reactive_statement: null,
 				component_slots: new Set(),
 				expression: null,
-				derived_state: [],
+				class_state: null,
 				function_depth: scope.function_depth
 			};
 
diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
index 17c8123de1fe..5f9887e98980 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
+++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
@@ -1,6 +1,7 @@
 import type { Scope } from '../scope.js';
 import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
 import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
+import type { ClassAnalysis } from './visitors/shared/class-analysis.js';
 
 export interface AnalysisState {
 	scope: Scope;
@@ -18,7 +19,9 @@ export interface AnalysisState {
 	component_slots: Set<string>;
 	/** Information about the current expression/directive/block value */
 	expression: ExpressionMetadata | null;
-	derived_state: { name: string; private: boolean }[];
+
+	/** Used to analyze class state. */
+	class_state: ClassAnalysis | null;
 	function_depth: number;
 
 	// legacy stuff
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js
index a64c89cd88f1..9a94fc668ed9 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js
@@ -23,5 +23,6 @@ export function AssignmentExpression(node, context) {
 		}
 	}
 
+	context.state.class_state?.register?.(node, context);
 	context.next();
 }
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index 683e343aa56a..2abf8a88b1d5 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -119,7 +119,10 @@ export function CallExpression(node, context) {
 				!(
 					call_expression_is_variable_declaration(parent, context) ||
 					call_expression_is_class_property_definition(parent) ||
-					call_expression_is_valid_class_property_assignment_in_constructor(parent, context)
+					context.state.class_state?.is_class_property_assignment_at_constructor_root(
+						parent,
+						context.path.slice(0, -1)
+					)
 				)
 			) {
 				e.state_invalid_placement(node, rune);
@@ -289,78 +292,3 @@ function call_expression_is_variable_declaration(parent, context) {
 function call_expression_is_class_property_definition(parent) {
 	return parent.type === 'PropertyDefinition' && !parent.static && !parent.computed;
 }
-
-/**
- *
- * @param {AST.SvelteNode} parent
- * @param {Context} context
- * @returns
- */
-function call_expression_is_valid_class_property_assignment_in_constructor(parent, context) {
-	return (
-		expression_is_assignment_to_top_level_property_of_this(parent) &&
-		current_node_is_in_constructor_root_or_control_flow_blocks(context)
-	);
-}
-
-/**
- * yes:
- *   - `this.foo = bar`
- *
- * no:
- *   - `this = bar`
- *   - `this.foo.baz = bar`
- *   - `anything_other_than_this = bar`
- *
- * @param {AST.SvelteNode} node
- */
-function expression_is_assignment_to_top_level_property_of_this(node) {
-	return (
-		node.type === 'AssignmentExpression' &&
-		node.operator === '=' &&
-		node.left.type === 'MemberExpression' &&
-		node.left.object.type === 'ThisExpression' &&
-		node.left.property.type === 'Identifier'
-	);
-}
-
-/**
- * @param {AST.SvelteNode} node
- */
-function node_is_constructor(node) {
-	return (
-		node.type === 'MethodDefinition' &&
-		node.key.type === 'Identifier' &&
-		node.key.name === 'constructor'
-	);
-}
-
-// if blocks are just IfStatements with BlockStatements or other IfStatements as consequents
-const allowed_parent_types = new Set([
-	'IfStatement',
-	'BlockStatement',
-	'SwitchCase',
-	'SwitchStatement'
-]);
-
-/**
- * Succeeds if the node's only direct parents are `if` / `else if` / `else` blocks _and_
- * those blocks are the direct children of the constructor.
- *
- * @param {Context} context
- */
-function current_node_is_in_constructor_root_or_control_flow_blocks(context) {
-	let parent_index = -3; // this gets us from CallExpression -> AssignmentExpression -> ExpressionStatement -> Whatever is here
-	while (true) {
-		const grandparent = get_parent(context.path, parent_index - 1);
-		const parent = get_parent(context.path, parent_index);
-		if (grandparent && node_is_constructor(grandparent)) {
-			// if this is the case then `parent` is the FunctionExpression
-			return true;
-		}
-		if (!allowed_parent_types.has(parent.type)) {
-			return false;
-		}
-		parent_index--;
-	}
-}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
deleted file mode 100644
index 0463e4da8563..000000000000
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/** @import { ClassBody } from 'estree' */
-/** @import { Context } from '../types' */
-import { get_rune } from '../../scope.js';
-
-/**
- * @param {ClassBody} node
- * @param {Context} context
- */
-export function ClassBody(node, context) {
-	/** @type {{name: string, private: boolean}[]} */
-	const derived_state = [];
-
-	for (const definition of node.body) {
-		if (
-			definition.type === 'PropertyDefinition' &&
-			(definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') &&
-			definition.value?.type === 'CallExpression'
-		) {
-			const rune = get_rune(definition.value, context.state.scope);
-			if (rune === '$derived' || rune === '$derived.by') {
-				derived_state.push({
-					name: definition.key.name,
-					private: definition.key.type === 'PrivateIdentifier'
-				});
-			}
-		}
-	}
-
-	context.next({ ...context.state, derived_state });
-}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js
index 20925a65b737..7e1df6aa2979 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js
@@ -1,6 +1,7 @@
 /** @import { ClassDeclaration } from 'estree' */
 /** @import { Context } from '../types' */
 import * as w from '../../../warnings.js';
+import { ClassAnalysis } from './shared/class-analysis.js';
 import { validate_identifier_name } from './shared/utils.js';
 
 /**
@@ -21,5 +22,5 @@ export function ClassDeclaration(node, context) {
 		w.perf_avoid_nested_class(node);
 	}
 
-	context.next();
+	context.next({ ...context.state, class_state: new ClassAnalysis() });
 }
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js
new file mode 100644
index 000000000000..6bf5eb73d5db
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js
@@ -0,0 +1,12 @@
+/** @import { PropertyDefinition } from 'estree' */
+/** @import { Context } from '../types' */
+
+/**
+ *
+ * @param {PropertyDefinition} node
+ * @param {Context} context
+ */
+export function PropertyDefinition(node, context) {
+	context.state.class_state?.register?.(node, context);
+	context.next();
+}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
new file mode 100644
index 000000000000..d6709eabc41e
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
@@ -0,0 +1,147 @@
+/** @import { AssignmentExpression, PropertyDefinition, Expression } from 'estree' */
+/** @import { AST } from '#compiler' */
+/** @import { Context } from '../../types' */
+
+import { get_parent } from '../../../../utils/ast.js';
+import { get_rune } from '../../../scope.js';
+import * as e from '../../../../errors.js';
+import { locate_node } from '../../../../state.js';
+
+/** @typedef {'$state' | '$state.raw' | '$derived' | '$derived.by' | 'regular'} PropertyAssignmentType */
+/** @typedef {{ type: PropertyAssignmentType; node: AssignmentExpression | PropertyDefinition; }} PropertyAssignmentDetails */
+
+const reassignable_assignments = new Set(['$state', '$state.raw', 'regular']);
+
+const property_assignment_types = new Set([
+	'$state',
+	'$state.raw',
+	'$derived',
+	'$derived.by',
+	'regular'
+]);
+
+export class ClassAnalysis {
+	// TODO: Probably need to include property definitions here too
+	/** @type {Map<string, PropertyAssignmentDetails>} */
+	property_assignments = new Map();
+
+	/**
+	 * Determines if the node is a valid assignment to a class property, and if so,
+	 * registers the assignment.
+	 * @param {AssignmentExpression | PropertyDefinition} node
+	 * @param {Context} context
+	 */
+	register(node, context) {
+		/** @type {string} */
+		let name;
+		/** @type {PropertyAssignmentType} */
+		let type;
+
+		if (node.type === 'AssignmentExpression') {
+			if (!this.is_class_property_assignment_at_constructor_root(node, context.path)) {
+				return;
+			}
+			name = node.left.property.name;
+			type = this.#get_assignment_type(node, context);
+
+			this.#check_for_conflicts(node, name, type);
+		} else {
+			if (!this.#is_assigned_property(node)) {
+				return;
+			}
+
+			name = node.key.name;
+			type = this.#get_assignment_type(node, context);
+
+			// we don't need to check for conflicts here because they're not possible yet
+		}
+
+		// we don't have to validate anything other than conflicts here, because the rune placement rules
+		// catch all of the other weirdness.
+		if (!this.property_assignments.has(name)) {
+			this.property_assignments.set(name, { type, node });
+		}
+	}
+
+	/**
+	 * @template {AST.SvelteNode} T
+	 * @param {AST.SvelteNode} node
+	 * @param {T[]} path
+	 * @returns {node is AssignmentExpression & { left: { type: 'MemberExpression' } & { object: { type: 'ThisExpression' }; property: { type: 'Identifier' } } }}
+	 */
+	is_class_property_assignment_at_constructor_root(node, path) {
+		if (
+			!(
+				node.type === 'AssignmentExpression' &&
+				node.operator === '=' &&
+				node.left.type === 'MemberExpression' &&
+				node.left.object.type === 'ThisExpression' &&
+				node.left.property.type === 'Identifier'
+			)
+		) {
+			return false;
+		}
+		// AssignmentExpression (here) -> ExpressionStatement (-1) -> BlockStatement (-2) -> FunctionExpression (-3) -> MethodDefinition (-4)
+		const maybe_constructor = get_parent(path, -4);
+		return (
+			maybe_constructor &&
+			maybe_constructor.type === 'MethodDefinition' &&
+			maybe_constructor.kind === 'constructor'
+		);
+	}
+
+	/**
+	 * We only care about properties that have values assigned to them -- if they don't,
+	 * they can't be a conflict for state declared in the constructor.
+	 * @param {PropertyDefinition} node
+	 * @returns {node is PropertyDefinition & { key: { type: 'PrivateIdentifier' | 'Identifier' }; value: Expression; static: false; computed: false }}
+	 */
+	#is_assigned_property(node) {
+		return (
+			(node.key.type === 'PrivateIdentifier' || node.key.type === 'Identifier') &&
+			Boolean(node.value) &&
+			!node.static &&
+			!node.computed
+		);
+	}
+
+	/**
+	 * Checks for conflicts with existing assignments. A conflict occurs if:
+	 * - The original assignment used `$derived` or `$derived.by` (these can never be reassigned)
+	 * - The original assignment used `$state`, `$state.raw`, or `regular` and is being assigned to with any type other than `regular`
+	 * @param {AssignmentExpression} node
+	 * @param {string} name
+	 * @param {PropertyAssignmentType} type
+	 */
+	#check_for_conflicts(node, name, type) {
+		const existing = this.property_assignments.get(name);
+		if (!existing) {
+			return;
+		}
+
+		if (reassignable_assignments.has(existing.type) && type === 'regular') {
+			return;
+		}
+
+		e.constructor_state_reassignment(node, name, locate_node(existing.node));
+	}
+
+	/**
+	 * @param {AssignmentExpression | PropertyDefinition} node
+	 * @param {Context} context
+	 * @returns {PropertyAssignmentType}
+	 */
+	#get_assignment_type(node, context) {
+		const value = node.type === 'AssignmentExpression' ? node.right : node.value;
+		const rune = get_rune(value, context.state.scope);
+		if (rune === null) {
+			return 'regular';
+		}
+		if (property_assignment_types.has(rune)) {
+			return /** @type {PropertyAssignmentType} */ (rune);
+		}
+		// this does mean we return `regular` for some other runes (like `$trace` or `$state.raw`)
+		// -- this is ok because the rune placement rules will throw if they're invalid.
+		return 'regular';
+	}
+}
diff --git a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js
index e6551031cc8e..f5b116117ed3 100644
--- a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js
@@ -4,7 +4,7 @@ export default test({
 	error: {
 		code: 'state_invalid_placement',
 		message:
-			'`$state(...)` can only be used as a variable declaration initializer or a class field',
+			'`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.',
 		position: [33, 41]
 	}
 });
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js
index 1185ca6b4548..592b2634a389 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js
@@ -3,6 +3,7 @@ import { test } from '../../test';
 export default test({
 	error: {
 		code: 'state_invalid_placement',
-		message: '`$state(...)` can only be used as a variable declaration initializer or a class field'
+		message:
+			'`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.'
 	}
 });
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js
index 476d7239a307..f31748d26a40 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js
@@ -4,6 +4,6 @@ export default test({
 	error: {
 		code: 'state_invalid_placement',
 		message:
-			'`$derived(...)` can only be used as a variable declaration initializer or a class field'
+			'`$derived(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.'
 	}
 });
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js
index 1185ca6b4548..592b2634a389 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js
@@ -3,6 +3,7 @@ import { test } from '../../test';
 export default test({
 	error: {
 		code: 'state_invalid_placement',
-		message: '`$state(...)` can only be used as a variable declaration initializer or a class field'
+		message:
+			'`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.'
 	}
 });
diff --git a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json
index 32594e426846..e1906b181a4d 100644
--- a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json
+++ b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json
@@ -1,7 +1,7 @@
 [
 	{
 		"code": "state_invalid_placement",
-		"message": "`$derived(...)` can only be used as a variable declaration initializer or a class field",
+		"message": "`$derived(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.",
 		"start": {
 			"line": 2,
 			"column": 15

From 033a466e846d198e90c9a2942d04f44b52951cdb Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Fri, 25 Apr 2025 14:54:34 -0600
Subject: [PATCH 03/10] misc

---
 .../2-analyze/visitors/ClassDeclaration.js       |  5 ++++-
 .../2-analyze/visitors/shared/class-analysis.js  | 10 ++++++----
 packages/svelte/src/utils.js                     | 16 +++++++++++++++-
 3 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js
index 7e1df6aa2979..b49ad099e59c 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassDeclaration.js
@@ -22,5 +22,8 @@ export function ClassDeclaration(node, context) {
 		w.perf_avoid_nested_class(node);
 	}
 
-	context.next({ ...context.state, class_state: new ClassAnalysis() });
+	context.next({
+		...context.state,
+		class_state: context.state.analysis.runes ? new ClassAnalysis() : null
+	});
 }
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
index d6709eabc41e..777762bc323f 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
@@ -6,6 +6,7 @@ import { get_parent } from '../../../../utils/ast.js';
 import { get_rune } from '../../../scope.js';
 import * as e from '../../../../errors.js';
 import { locate_node } from '../../../../state.js';
+import { is_state_creation_rune } from '../../../../../utils.js';
 
 /** @typedef {'$state' | '$state.raw' | '$derived' | '$derived.by' | 'regular'} PropertyAssignmentType */
 /** @typedef {{ type: PropertyAssignmentType; node: AssignmentExpression | PropertyDefinition; }} PropertyAssignmentDetails */
@@ -67,7 +68,7 @@ export class ClassAnalysis {
 	 * @template {AST.SvelteNode} T
 	 * @param {AST.SvelteNode} node
 	 * @param {T[]} path
-	 * @returns {node is AssignmentExpression & { left: { type: 'MemberExpression' } & { object: { type: 'ThisExpression' }; property: { type: 'Identifier' } } }}
+	 * @returns {node is AssignmentExpression & { left: { type: 'MemberExpression' } & { object: { type: 'ThisExpression' }; property: { type: 'Identifier' | 'PrivateIdentifier' } } }}
 	 */
 	is_class_property_assignment_at_constructor_root(node, path) {
 		if (
@@ -76,7 +77,8 @@ export class ClassAnalysis {
 				node.operator === '=' &&
 				node.left.type === 'MemberExpression' &&
 				node.left.object.type === 'ThisExpression' &&
-				node.left.property.type === 'Identifier'
+				(node.left.property.type === 'Identifier' ||
+					node.left.property.type === 'PrivateIdentifier')
 			)
 		) {
 			return false;
@@ -137,8 +139,8 @@ export class ClassAnalysis {
 		if (rune === null) {
 			return 'regular';
 		}
-		if (property_assignment_types.has(rune)) {
-			return /** @type {PropertyAssignmentType} */ (rune);
+		if (is_state_creation_rune(rune)) {
+			return rune;
 		}
 		// this does mean we return `regular` for some other runes (like `$trace` or `$state.raw`)
 		// -- this is ok because the rune placement rules will throw if they're invalid.
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index ada318e85ac7..2758b9fcbc19 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -447,14 +447,28 @@ const RUNES = /** @type {const} */ ([
 	'$host'
 ]);
 
+/** @typedef {RUNES[number]} RuneName */
+
 /**
  * @param {string} name
- * @returns {name is RUNES[number]}
+ * @returns {name is RuneName}
  */
 export function is_rune(name) {
 	return RUNES.includes(/** @type {RUNES[number]} */ (name));
 }
 
+/** @typedef {'$state' | '$state.raw' | '$derived' | '$derived.by'} StateCreationRuneName */
+
+/**
+ * @param {string} name
+ * @returns {name is StateCreationRuneName}
+ */
+export function is_state_creation_rune(name) {
+	return (
+		name === '$state' || name === '$state.raw' || name === '$derived' || name === '$derived.by'
+	);
+}
+
 /** List of elements that require raw contents and should not have SSR comments put in them */
 const RAW_TEXT_ELEMENTS = /** @type {const} */ (['textarea', 'script', 'style', 'title']);
 

From 005ba299684a1f30f1d462aac994fe1884de512a Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Fri, 25 Apr 2025 20:19:58 -0600
Subject: [PATCH 04/10] feat: client

---
 .../3-transform/client/transform-client.js    |   8 +-
 .../phases/3-transform/client/types.d.ts      |   7 +-
 .../client/visitors/AssignmentExpression.js   |   7 +-
 .../3-transform/client/visitors/ClassBody.js  | 176 ++--------
 .../client/visitors/MemberExpression.js       |   5 +-
 .../client/visitors/UpdateExpression.js       |   2 +-
 .../client/visitors/shared/class-analysis.js  | 301 ++++++++++++++++++
 .../3-transform/server/visitors/ClassBody.js  |   6 +-
 8 files changed, 344 insertions(+), 168 deletions(-)
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js

diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index f0da5a491887..ff448f31a126 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -161,8 +161,7 @@ export function client_component(analysis, options) {
 		},
 		events: new Set(),
 		preserve_whitespace: options.preserveWhitespace,
-		public_state: new Map(),
-		private_state: new Map(),
+		class_analysis: null,
 		transform: {},
 		in_constructor: false,
 		instance_level_snippets: [],
@@ -669,10 +668,9 @@ export function client_module(analysis, options) {
 		options,
 		scope: analysis.module.scope,
 		scopes: analysis.module.scopes,
-		public_state: new Map(),
-		private_state: new Map(),
 		transform: {},
-		in_constructor: false
+		in_constructor: false,
+		class_analysis: null
 	};
 
 	const module = /** @type {ESTree.Program} */ (
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
index 243e1c64a33c..2ab5bd1a6094 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
@@ -13,10 +13,11 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
 import type { TransformState } from '../types.js';
 import type { ComponentAnalysis } from '../../types.js';
 import type { SourceLocation } from '#shared';
+import type { StateCreationRuneName } from '../../../../utils.js';
+import type { ClassAnalysis } from './visitors/shared/class-analysis.js';
 
 export interface ClientTransformState extends TransformState {
-	readonly private_state: Map<string, StateField>;
-	readonly public_state: Map<string, StateField>;
+	readonly class_analysis: ClassAnalysis | null;
 
 	/**
 	 * `true` if the current lexical scope belongs to a class constructor. this allows
@@ -95,7 +96,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
 }
 
 export interface StateField {
-	kind: 'state' | 'raw_state' | 'derived' | 'derived_by';
+	kind: StateCreationRuneName;
 	id: PrivateIdentifier;
 }
 
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
index e3f945005051..d045b83298f3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
@@ -1,4 +1,4 @@
-/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */
+/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern, MemberExpression, ThisExpression, PrivateIdentifier, CallExpression } from 'estree' */
 /** @import { AST } from '#compiler' */
 /** @import { Context } from '../types.js' */
 import * as b from '#compiler/builders';
@@ -17,6 +17,7 @@ import { validate_mutation } from './shared/utils.js';
  * @param {Context} context
  */
 export function AssignmentExpression(node, context) {
+	context.state.class_analysis?.register_assignment(node, context);
 	const expression = /** @type {Expression} */ (
 		visit_assignment_expression(node, context, build_assignment) ?? context.next()
 	);
@@ -56,7 +57,7 @@ function build_assignment(operator, left, right, context) {
 		left.type === 'MemberExpression' &&
 		left.property.type === 'PrivateIdentifier'
 	) {
-		const private_state = context.state.private_state.get(left.property.name);
+		const private_state = context.state.class_analysis?.private_state.get(left.property.name);
 
 		if (private_state !== undefined) {
 			let value = /** @type {Expression} */ (
@@ -64,7 +65,7 @@ function build_assignment(operator, left, right, context) {
 			);
 
 			const needs_proxy =
-				private_state.kind === 'state' &&
+				private_state.kind === '$state' &&
 				is_non_coercive_operator(operator) &&
 				should_proxy(value, context.state.scope);
 
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
index f3ebd940699c..534fdba44672 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
@@ -1,9 +1,7 @@
-/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
+/** @import { ClassBody, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
 /** @import { Context, StateField } from '../types' */
-import * as b from '#compiler/builders';
 import { regex_invalid_identifier_chars } from '../../../patterns.js';
-import { get_rune } from '../../../scope.js';
-import { should_proxy } from '../utils.js';
+import { ClassAnalysis } from './shared/class-analysis.js';
 
 /**
  * @param {ClassBody} node
@@ -15,170 +13,46 @@ export function ClassBody(node, context) {
 		return;
 	}
 
-	/** @type {Map<string, StateField>} */
-	const public_state = new Map();
-
-	/** @type {Map<string, StateField>} */
-	const private_state = new Map();
-
-	/** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */
-	const definition_names = new Map();
-
-	/** @type {string[]} */
-	const private_ids = [];
+	const class_analysis = new ClassAnalysis();
 
 	for (const definition of node.body) {
-		if (
-			(definition.type === 'PropertyDefinition' || definition.type === 'MethodDefinition') &&
-			(definition.key.type === 'Identifier' ||
-				definition.key.type === 'PrivateIdentifier' ||
-				definition.key.type === 'Literal')
-		) {
-			const type = definition.key.type;
-			const name = get_name(definition.key, public_state);
-			if (!name) continue;
-
-			// we store the deconflicted name in the map so that we can access it later
-			definition_names.set(definition.key, name);
-
-			const is_private = type === 'PrivateIdentifier';
-			if (is_private) private_ids.push(name);
-
-			if (definition.value?.type === 'CallExpression') {
-				const rune = get_rune(definition.value, context.state.scope);
-				if (
-					rune === '$state' ||
-					rune === '$state.raw' ||
-					rune === '$derived' ||
-					rune === '$derived.by'
-				) {
-					/** @type {StateField} */
-					const field = {
-						kind:
-							rune === '$state'
-								? 'state'
-								: rune === '$state.raw'
-									? 'raw_state'
-									: rune === '$derived.by'
-										? 'derived_by'
-										: 'derived',
-						// @ts-expect-error this is set in the next pass
-						id: is_private ? definition.key : null
-					};
-
-					if (is_private) {
-						private_state.set(name, field);
-					} else {
-						public_state.set(name, field);
-					}
-				}
-			}
-		}
+		class_analysis.register_body_definition(definition, context.state.scope);
 	}
 
-	// each `foo = $state()` needs a backing `#foo` field
-	for (const [name, field] of public_state) {
-		let deconflicted = name;
-		while (private_ids.includes(deconflicted)) {
-			deconflicted = '_' + deconflicted;
-		}
-
-		private_ids.push(deconflicted);
-		field.id = b.private_id(deconflicted);
-	}
+	class_analysis.finalize_property_definitions();
 
 	/** @type {Array<MethodDefinition | PropertyDefinition>} */
 	const body = [];
 
-	const child_state = { ...context.state, public_state, private_state };
+	const child_state = {
+		...context.state,
+		class_analysis
+	};
+
+	// we need to visit the constructor first so that it can add to the field maps.
+	const constructor_node = node.body.find(
+		(child) => child.type === 'MethodDefinition' && child.kind === 'constructor'
+	);
+	const constructor = constructor_node && context.visit(constructor_node, child_state);
 
 	// Replace parts of the class body
 	for (const definition of node.body) {
-		if (
-			definition.type === 'PropertyDefinition' &&
-			(definition.key.type === 'Identifier' ||
-				definition.key.type === 'PrivateIdentifier' ||
-				definition.key.type === 'Literal')
-		) {
-			const name = definition_names.get(definition.key);
-			if (!name) continue;
-
-			const is_private = definition.key.type === 'PrivateIdentifier';
-			const field = (is_private ? private_state : public_state).get(name);
-
-			if (definition.value?.type === 'CallExpression' && field !== undefined) {
-				let value = null;
-
-				if (definition.value.arguments.length > 0) {
-					const init = /** @type {Expression} **/ (
-						context.visit(definition.value.arguments[0], child_state)
-					);
-
-					value =
-						field.kind === 'state'
-							? b.call(
-									'$.state',
-									should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
-								)
-							: field.kind === 'raw_state'
-								? b.call('$.state', init)
-								: field.kind === 'derived_by'
-									? b.call('$.derived', init)
-									: b.call('$.derived', b.thunk(init));
-				} else {
-					// if no arguments, we know it's state as `$derived()` is a compile error
-					value = b.call('$.state');
-				}
-
-				if (is_private) {
-					body.push(b.prop_def(field.id, value));
-				} else {
-					// #foo;
-					const member = b.member(b.this, field.id);
-					body.push(b.prop_def(field.id, value));
-
-					// get foo() { return this.#foo; }
-					body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
+		if (definition === constructor_node) {
+			body.push(/** @type {MethodDefinition} */ (constructor));
+			continue;
+		}
 
-					// set foo(value) { this.#foo = value; }
-					const val = b.id('value');
+		const state_field = class_analysis.build_state_field_from_body_definition(definition, context);
 
-					body.push(
-						b.method(
-							'set',
-							definition.key,
-							[val],
-							[b.stmt(b.call('$.set', member, val, field.kind === 'state' && b.true))]
-						)
-					);
-				}
-				continue;
-			}
+		if (state_field) {
+			body.push(...state_field);
+			continue;
 		}
 
 		body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
 	}
 
-	return { ...node, body };
-}
+	body.push(...class_analysis.constructor_state_fields);
 
-/**
- * @param {Identifier | PrivateIdentifier | Literal} node
- * @param {Map<string, StateField>} public_state
- */
-function get_name(node, public_state) {
-	if (node.type === 'Literal') {
-		let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
-
-		// the above could generate conflicts because it has to generate a valid identifier
-		// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
-		// so we have to de-conflict. We can only check `public_state` because private state
-		// can't have literal keys
-		while (name && public_state.has(name)) {
-			name = '_' + name;
-		}
-		return name;
-	} else {
-		return node.name;
-	}
+	return { ...node, body };
 }
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
index ab88345ddd3c..410c9ad9ab79 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
@@ -9,9 +9,10 @@ import * as b from '#compiler/builders';
 export function MemberExpression(node, context) {
 	// rewrite `this.#foo` as `this.#foo.v` inside a constructor
 	if (node.property.type === 'PrivateIdentifier') {
-		const field = context.state.private_state.get(node.property.name);
+		const field = context.state.class_analysis?.private_state.get(node.property.name);
 		if (field) {
-			return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === 'state')
+			return context.state.in_constructor &&
+				(field.kind === '$state.raw' || field.kind === '$state')
 				? b.member(node, 'v')
 				: b.call('$.get', node);
 		}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
index 96be119b840a..c1f7c390ced9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
@@ -15,7 +15,7 @@ export function UpdateExpression(node, context) {
 		argument.type === 'MemberExpression' &&
 		argument.object.type === 'ThisExpression' &&
 		argument.property.type === 'PrivateIdentifier' &&
-		context.state.private_state.has(argument.property.name)
+		context.state.class_analysis?.private_state.has(argument.property.name)
 	) {
 		let fn = '$.update';
 		if (node.prefix) fn += '_pre';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js
new file mode 100644
index 000000000000..77618a482c12
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js
@@ -0,0 +1,301 @@
+/** @import { Context, StateField } from '../../types.js' */
+/** @import { AssignmentExpression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, CallExpression, Expression, StaticBlock, SpreadElement } from 'estree' */
+/** @import { Scope } from '#compiler' */
+/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
+import * as b from '#compiler/builders';
+import { is_state_creation_rune } from '../../../../../../utils.js';
+import { regex_invalid_identifier_chars } from '../../../../patterns.js';
+import { get_rune } from '../../../../scope.js';
+import { should_proxy } from '../../utils.js';
+
+export class ClassAnalysis {
+	/** @type {Map<string, StateField>} */
+	public_state = new Map();
+
+	/** @type {Map<string, StateField>} */
+	private_state = new Map();
+
+	/**
+	 * Any state fields discovered from {@link register_assignment} that need to be added to the class body.
+	 * @type {Array<PropertyDefinition | MethodDefinition>}
+	 */
+	constructor_state_fields = [];
+
+	/** @type {Map<(MethodDefinition | PropertyDefinition)["key"], string>} */
+	#definition_names = new Map();
+
+	/** @type {Set<string>} */
+	#private_ids = new Set();
+
+	/**
+	 * @param {MethodDefinition | PropertyDefinition | StaticBlock} node
+	 * @param {Scope} scope
+	 */
+	register_body_definition(node, scope) {
+		if (
+			!(
+				(node.type === 'PropertyDefinition' || node.type === 'MethodDefinition') &&
+				(node.key.type === 'Identifier' ||
+					node.key.type === 'PrivateIdentifier' ||
+					node.key.type === 'Literal')
+			)
+		) {
+			return;
+		}
+
+		/*
+		 * We don't know if the node is stateful yet, but we still need to register some details.
+		 * For example: If the node is a private identifier, we could accidentally conflict with it later
+		 * if we create a private field for public state (as would happen in this example:)
+		 *
+		 * ```ts
+		 * class Foo {
+		 *   #count = 0;
+		 *   count = $state(0); // would become #count if we didn't know about the private field above
+		 * }
+		 */
+
+		const name = ClassAnalysis.#get_name(node.key, this.public_state);
+		if (!name) {
+			return;
+		}
+
+		// we store the deconflicted name in the map so that we can access it later
+		this.#definition_names.set(node.key, name);
+
+		const is_private = node.key.type === 'PrivateIdentifier';
+		if (is_private) {
+			this.#private_ids.add(name);
+		}
+
+		const rune = get_rune(node.value, scope);
+		if (!rune || !is_state_creation_rune(rune)) {
+			return;
+		}
+
+		if (is_private) {
+			this.private_state.set(name, { kind: rune, id: /** @type {PrivateIdentifier} */ (node.key) });
+		} else {
+			// We can't set the ID until we've identified all of the private state fields,
+			// otherwise we might conflict with them. After registering all property definitions,
+			// call `finalize_property_definitions` to populate the IDs.
+			// @ts-expect-error this is set in `finalize_property_definitions`
+			this.public_state.set(name, { kind: rune, id: undefined });
+		}
+	}
+
+	/**
+	 * Resolves all of the registered public state fields to their final private IDs.
+	 * Must be called after all property definitions have been registered.
+	 */
+	finalize_property_definitions() {
+		for (const [name, field] of this.public_state) {
+			field.id = this.#deconflict(name);
+		}
+	}
+
+	/**
+	 * Important note: It is a syntax error in JavaScript to try to assign to a private class field
+	 * that was not declared in the class body. So there is absolutely no risk of unresolvable conflicts here.
+	 *
+	 * This function will modify the assignment expression passed to it if it is registered as a state field.
+	 * @param {AssignmentExpression} node
+	 * @param {Context} context
+	 */
+	register_assignment(node, context) {
+		if (
+			!(
+				node.operator === '=' &&
+				node.left.type === 'MemberExpression' &&
+				node.left.object.type === 'ThisExpression' &&
+				node.left.property.type === 'Identifier'
+			)
+		) {
+			return;
+		}
+
+		const name = ClassAnalysis.#get_name(node.left.property, this.public_state);
+		if (!name) {
+			return;
+		}
+
+		const rune = get_rune(node.right, context.state.scope);
+		if (!rune || !is_state_creation_rune(rune)) {
+			return false;
+		}
+
+		const id = this.#deconflict(name);
+		const field = { kind: rune, id };
+		this.public_state.set(name, field);
+
+		// We need to do two things:
+		// - Communicate to the class body visitor that it needs to append nodes to create a state field
+		// - Modify the assignment expression so that it's valid
+		this.constructor_state_fields.push(
+			...this.build_state_field(
+				false,
+				field,
+				node.left.property,
+				// this will initialize the field without assigning to it, delegating to the constructor
+				null,
+				context
+			)
+		);
+
+		// ...swap out the assignment to go directly against the private field
+		node.left.property = id;
+		// ...and swap out the assignment's value for the state field init
+		node.right = this.#build_init_value(
+			rune,
+			/** @type {CallExpression} */ (node.right).arguments[0],
+			context
+		);
+	}
+
+	/**
+	 *
+	 * @param {PropertyDefinition | MethodDefinition | StaticBlock} node
+	 * @param {Context} context
+	 * @returns {Array<PropertyDefinition | MethodDefinition> | null}
+	 */
+	build_state_field_from_body_definition(node, context) {
+		if (
+			!(
+				node.type === 'PropertyDefinition' &&
+				(node.key.type === 'Identifier' ||
+					node.key.type === 'PrivateIdentifier' ||
+					node.key.type === 'Literal')
+			)
+		) {
+			return null;
+		}
+
+		const name = this.#definition_names.get(node.key);
+		if (!name) throw new Error('This should not be possible'); // TODO
+
+		const is_private = node.key.type === 'PrivateIdentifier';
+		const field = (is_private ? this.private_state : this.public_state).get(name);
+
+		if (!field) {
+			return null;
+		}
+
+		return this.build_state_field(
+			is_private,
+			field,
+			node.key,
+			// we can cast this because if we have a field for this definition it definitely is a call
+			// expression, otherwise it wouldn't have produced a rune earlier.
+			/** @type {CallExpression} */ (node.value),
+			context
+		);
+	}
+
+	/**
+	 * @param {boolean} is_private
+	 * @param {StateField} field
+	 * @param {(MethodDefinition | PropertyDefinition)["key"]} original_definition_id
+	 * @param {CallExpression | null} call_expression
+	 * @param {Context} context
+	 *
+	 * @returns {Array<PropertyDefinition | MethodDefinition>}
+	 */
+	build_state_field(is_private, field, original_definition_id, call_expression, context) {
+		let value;
+		if (!call_expression) {
+			// if there's no call expression, this is state that's created in the constructor.
+			// it's guaranteed to be the very first assignment to this field, so we initialize
+			// the field but don't assign to it.
+			value = null;
+		} else if (call_expression.arguments.length > 0) {
+			value = this.#build_init_value(field.kind, call_expression.arguments[0], context);
+		} else {
+			// if no arguments, we know it's state as `$derived()` is a compile error
+			value = b.call('$.state');
+		}
+
+		if (is_private) {
+			return [b.prop_def(field.id, value)];
+		}
+
+		const member = b.member(b.this, field.id);
+		const val = b.id('value');
+		return [
+			// #foo;
+			b.prop_def(field.id, value),
+			// get foo() { return this.#foo; }
+			b.method('get', original_definition_id, [], [b.return(b.call('$.get', member))]),
+			// set foo(value) { this.#foo = value; }
+			b.method(
+				'set',
+				original_definition_id,
+				[val],
+				[b.stmt(b.call('$.set', member, val, field.kind === '$state' && b.true))]
+			)
+		];
+	}
+
+	/**
+	 *
+	 * @param {StateCreationRuneName} kind
+	 * @param {Expression | SpreadElement} arg
+	 * @param {Context} context
+	 */
+	#build_init_value(kind, arg, context) {
+		const init = /** @type {Expression} **/ (
+			context.visit(arg, {
+				...context.state,
+				class_analysis: this
+			})
+		);
+
+		switch (kind) {
+			case '$state':
+				return b.call(
+					'$.state',
+					should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
+				);
+			case '$state.raw':
+				return b.call('$.state', init);
+			case '$derived':
+				return b.call('$.derived', b.thunk(init));
+			case '$derived.by':
+				return b.call('$.derived', init);
+		}
+	}
+
+	/**
+	 * @param {string} name
+	 * @returns {PrivateIdentifier}
+	 */
+	#deconflict(name) {
+		let deconflicted = name;
+		while (this.#private_ids.has(deconflicted)) {
+			deconflicted = '_' + deconflicted;
+		}
+
+		this.#private_ids.add(deconflicted);
+		return b.private_id(deconflicted);
+	}
+
+	/**
+	 * @param {Identifier | PrivateIdentifier | Literal} node
+	 * @param {Map<string, unknown>} public_state
+	 */
+	static #get_name(node, public_state) {
+		if (node.type === 'Literal') {
+			let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
+
+			// the above could generate conflicts because it has to generate a valid identifier
+			// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
+			// so we have to de-conflict. We can only check `public_state` because private state
+			// can't have literal keys
+			while (name && public_state.has(name)) {
+				name = '_' + name;
+			}
+			return name;
+		} else {
+			return node.name;
+		}
+	}
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
index c0ebdeae0822..ff4a1206ddaa 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
@@ -39,7 +39,7 @@ export function ClassBody(node, context) {
 				if (rune === '$derived' || rune === '$derived.by') {
 					/** @type {StateField} */
 					const field = {
-						kind: rune === '$derived.by' ? 'derived_by' : 'derived',
+						kind: rune,
 						// @ts-expect-error this is set in the next pass
 						id: is_private ? definition.key : null
 					};
@@ -86,7 +86,7 @@ export function ClassBody(node, context) {
 					context.visit(definition.value.arguments[0], child_state)
 				);
 				const value =
-					field.kind === 'derived_by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
+					field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
 
 				if (is_private) {
 					body.push(b.prop_def(field.id, value));
@@ -98,7 +98,7 @@ export function ClassBody(node, context) {
 					// get foo() { return this.#foo; }
 					body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
 
-					if (dev && (field.kind === 'derived' || field.kind === 'derived_by')) {
+					if (dev && (field.kind === '$derived' || field.kind === '$derived.by')) {
 						body.push(
 							b.method(
 								'set',

From 4d6422cca425de4cccdddec4632e638959b18296 Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Fri, 25 Apr 2025 20:44:59 -0600
Subject: [PATCH 05/10] improvements

---
 .../2-analyze/visitors/shared/class-analysis.js       | 11 ++---------
 .../client/visitors/shared/class-analysis.js          |  2 +-
 2 files changed, 3 insertions(+), 10 deletions(-)

diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
index 777762bc323f..4b3d758200b6 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
@@ -1,6 +1,7 @@
 /** @import { AssignmentExpression, PropertyDefinition, Expression } from 'estree' */
 /** @import { AST } from '#compiler' */
 /** @import { Context } from '../../types' */
+/** @import { StateCreationRuneName } from '../../../../../utils.js' */
 
 import { get_parent } from '../../../../utils/ast.js';
 import { get_rune } from '../../../scope.js';
@@ -8,19 +9,11 @@ import * as e from '../../../../errors.js';
 import { locate_node } from '../../../../state.js';
 import { is_state_creation_rune } from '../../../../../utils.js';
 
-/** @typedef {'$state' | '$state.raw' | '$derived' | '$derived.by' | 'regular'} PropertyAssignmentType */
+/** @typedef { StateCreationRuneName | 'regular'} PropertyAssignmentType */
 /** @typedef {{ type: PropertyAssignmentType; node: AssignmentExpression | PropertyDefinition; }} PropertyAssignmentDetails */
 
 const reassignable_assignments = new Set(['$state', '$state.raw', 'regular']);
 
-const property_assignment_types = new Set([
-	'$state',
-	'$state.raw',
-	'$derived',
-	'$derived.by',
-	'regular'
-]);
-
 export class ClassAnalysis {
 	// TODO: Probably need to include property definitions here too
 	/** @type {Map<string, PropertyAssignmentDetails>} */
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js
index 77618a482c12..0ce4451e8ba2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js
@@ -121,7 +121,7 @@ export class ClassAnalysis {
 
 		const rune = get_rune(node.right, context.state.scope);
 		if (!rune || !is_state_creation_rune(rune)) {
-			return false;
+			return;
 		}
 
 		const id = this.#deconflict(name);

From adb6e712e90133817cf4ea76430436d69213c675 Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Mon, 28 Apr 2025 23:23:23 -0400
Subject: [PATCH 06/10] feat: It is now at least backwards compatible. though
 the new stuff may still be wrong

---
 .../phases/3-transform/client/types.d.ts      |  11 +-
 .../client/visitors/AssignmentExpression.js   |   4 +-
 .../3-transform/client/visitors/ClassBody.js  |  49 +--
 .../client/visitors/MemberExpression.js       |   2 +-
 .../client/visitors/UpdateExpression.js       |   2 +-
 .../client/visitors/shared/class-analysis.js  | 301 ----------------
 .../visitors/shared/client-class-analysis.js  |  85 +++++
 .../3-transform/server/transform-server.js    |   4 +-
 .../phases/3-transform/server/types.d.ts      |   4 +-
 .../3-transform/server/visitors/ClassBody.js  | 110 +-----
 .../server/visitors/MemberExpression.js       |   5 +-
 .../visitors/shared/server-class-analysis.js  |  71 ++++
 .../3-transform/shared/class_analysis.js      | 334 ++++++++++++++++++
 .../phases/3-transform/shared/types.d.ts      |  63 ++++
 .../compiler/phases/3-transform/types.d.ts    |   7 +
 packages/svelte/src/utils.js                  |  11 +-
 .../_expected/client/index.svelte.js          |  10 +-
 .../_expected/server/index.svelte.js          |   6 +-
 18 files changed, 598 insertions(+), 481 deletions(-)
 delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts

diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
index 2ab5bd1a6094..3e7ef24aee58 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
@@ -3,7 +3,6 @@ import type {
 	Statement,
 	LabeledStatement,
 	Identifier,
-	PrivateIdentifier,
 	Expression,
 	AssignmentExpression,
 	UpdateExpression,
@@ -13,11 +12,10 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
 import type { TransformState } from '../types.js';
 import type { ComponentAnalysis } from '../../types.js';
 import type { SourceLocation } from '#shared';
-import type { StateCreationRuneName } from '../../../../utils.js';
-import type { ClassAnalysis } from './visitors/shared/class-analysis.js';
+import type { ClassAnalysis } from '../shared/types.js';
 
 export interface ClientTransformState extends TransformState {
-	readonly class_analysis: ClassAnalysis | null;
+	readonly class_analysis: ClassAnalysis<Context> | null;
 
 	/**
 	 * `true` if the current lexical scope belongs to a class constructor. this allows
@@ -95,11 +93,6 @@ export interface ComponentClientTransformState extends ClientTransformState {
 	readonly module_level_snippets: VariableDeclaration[];
 }
 
-export interface StateField {
-	kind: StateCreationRuneName;
-	id: PrivateIdentifier;
-}
-
 export type Context = import('zimmerframe').Context<AST.SvelteNode, ClientTransformState>;
 export type Visitors = import('zimmerframe').Visitors<AST.SvelteNode, any>;
 
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
index d045b83298f3..7d0840ab8098 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
@@ -57,7 +57,7 @@ function build_assignment(operator, left, right, context) {
 		left.type === 'MemberExpression' &&
 		left.property.type === 'PrivateIdentifier'
 	) {
-		const private_state = context.state.class_analysis?.private_state.get(left.property.name);
+		const private_state = context.state.class_analysis?.get_field(left.property.name, true);
 
 		if (private_state !== undefined) {
 			let value = /** @type {Expression} */ (
@@ -65,7 +65,7 @@ function build_assignment(operator, left, right, context) {
 			);
 
 			const needs_proxy =
-				private_state.kind === '$state' &&
+				private_state?.kind === '$state' &&
 				is_non_coercive_operator(operator) &&
 				should_proxy(value, context.state.scope);
 
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
index 534fdba44672..a9ccad03770c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
@@ -1,7 +1,6 @@
-/** @import { ClassBody, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
-/** @import { Context, StateField } from '../types' */
-import { regex_invalid_identifier_chars } from '../../../patterns.js';
-import { ClassAnalysis } from './shared/class-analysis.js';
+/** @import { ClassBody } from 'estree' */
+/** @import { Context } from '../types' */
+import { create_client_class_analysis } from './shared/client-class-analysis.js';
 
 /**
  * @param {ClassBody} node
@@ -13,46 +12,8 @@ export function ClassBody(node, context) {
 		return;
 	}
 
-	const class_analysis = new ClassAnalysis();
-
-	for (const definition of node.body) {
-		class_analysis.register_body_definition(definition, context.state.scope);
-	}
-
-	class_analysis.finalize_property_definitions();
-
-	/** @type {Array<MethodDefinition | PropertyDefinition>} */
-	const body = [];
-
-	const child_state = {
-		...context.state,
-		class_analysis
-	};
-
-	// we need to visit the constructor first so that it can add to the field maps.
-	const constructor_node = node.body.find(
-		(child) => child.type === 'MethodDefinition' && child.kind === 'constructor'
-	);
-	const constructor = constructor_node && context.visit(constructor_node, child_state);
-
-	// Replace parts of the class body
-	for (const definition of node.body) {
-		if (definition === constructor_node) {
-			body.push(/** @type {MethodDefinition} */ (constructor));
-			continue;
-		}
-
-		const state_field = class_analysis.build_state_field_from_body_definition(definition, context);
-
-		if (state_field) {
-			body.push(...state_field);
-			continue;
-		}
-
-		body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
-	}
-
-	body.push(...class_analysis.constructor_state_fields);
+	const class_analysis = create_client_class_analysis(node.body);
+	const body = class_analysis.generate_body(context);
 
 	return { ...node, body };
 }
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
index 410c9ad9ab79..87a886fd3b78 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js
@@ -9,7 +9,7 @@ import * as b from '#compiler/builders';
 export function MemberExpression(node, context) {
 	// rewrite `this.#foo` as `this.#foo.v` inside a constructor
 	if (node.property.type === 'PrivateIdentifier') {
-		const field = context.state.class_analysis?.private_state.get(node.property.name);
+		const field = context.state.class_analysis?.get_field(node.property.name, true);
 		if (field) {
 			return context.state.in_constructor &&
 				(field.kind === '$state.raw' || field.kind === '$state')
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
index c1f7c390ced9..2e6f9123ddc9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js
@@ -15,7 +15,7 @@ export function UpdateExpression(node, context) {
 		argument.type === 'MemberExpression' &&
 		argument.object.type === 'ThisExpression' &&
 		argument.property.type === 'PrivateIdentifier' &&
-		context.state.class_analysis?.private_state.has(argument.property.name)
+		context.state.class_analysis?.get_field(argument.property.name, true)
 	) {
 		let fn = '$.update';
 		if (node.prefix) fn += '_pre';
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js
deleted file mode 100644
index 0ce4451e8ba2..000000000000
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/class-analysis.js
+++ /dev/null
@@ -1,301 +0,0 @@
-/** @import { Context, StateField } from '../../types.js' */
-/** @import { AssignmentExpression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, CallExpression, Expression, StaticBlock, SpreadElement } from 'estree' */
-/** @import { Scope } from '#compiler' */
-/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
-import * as b from '#compiler/builders';
-import { is_state_creation_rune } from '../../../../../../utils.js';
-import { regex_invalid_identifier_chars } from '../../../../patterns.js';
-import { get_rune } from '../../../../scope.js';
-import { should_proxy } from '../../utils.js';
-
-export class ClassAnalysis {
-	/** @type {Map<string, StateField>} */
-	public_state = new Map();
-
-	/** @type {Map<string, StateField>} */
-	private_state = new Map();
-
-	/**
-	 * Any state fields discovered from {@link register_assignment} that need to be added to the class body.
-	 * @type {Array<PropertyDefinition | MethodDefinition>}
-	 */
-	constructor_state_fields = [];
-
-	/** @type {Map<(MethodDefinition | PropertyDefinition)["key"], string>} */
-	#definition_names = new Map();
-
-	/** @type {Set<string>} */
-	#private_ids = new Set();
-
-	/**
-	 * @param {MethodDefinition | PropertyDefinition | StaticBlock} node
-	 * @param {Scope} scope
-	 */
-	register_body_definition(node, scope) {
-		if (
-			!(
-				(node.type === 'PropertyDefinition' || node.type === 'MethodDefinition') &&
-				(node.key.type === 'Identifier' ||
-					node.key.type === 'PrivateIdentifier' ||
-					node.key.type === 'Literal')
-			)
-		) {
-			return;
-		}
-
-		/*
-		 * We don't know if the node is stateful yet, but we still need to register some details.
-		 * For example: If the node is a private identifier, we could accidentally conflict with it later
-		 * if we create a private field for public state (as would happen in this example:)
-		 *
-		 * ```ts
-		 * class Foo {
-		 *   #count = 0;
-		 *   count = $state(0); // would become #count if we didn't know about the private field above
-		 * }
-		 */
-
-		const name = ClassAnalysis.#get_name(node.key, this.public_state);
-		if (!name) {
-			return;
-		}
-
-		// we store the deconflicted name in the map so that we can access it later
-		this.#definition_names.set(node.key, name);
-
-		const is_private = node.key.type === 'PrivateIdentifier';
-		if (is_private) {
-			this.#private_ids.add(name);
-		}
-
-		const rune = get_rune(node.value, scope);
-		if (!rune || !is_state_creation_rune(rune)) {
-			return;
-		}
-
-		if (is_private) {
-			this.private_state.set(name, { kind: rune, id: /** @type {PrivateIdentifier} */ (node.key) });
-		} else {
-			// We can't set the ID until we've identified all of the private state fields,
-			// otherwise we might conflict with them. After registering all property definitions,
-			// call `finalize_property_definitions` to populate the IDs.
-			// @ts-expect-error this is set in `finalize_property_definitions`
-			this.public_state.set(name, { kind: rune, id: undefined });
-		}
-	}
-
-	/**
-	 * Resolves all of the registered public state fields to their final private IDs.
-	 * Must be called after all property definitions have been registered.
-	 */
-	finalize_property_definitions() {
-		for (const [name, field] of this.public_state) {
-			field.id = this.#deconflict(name);
-		}
-	}
-
-	/**
-	 * Important note: It is a syntax error in JavaScript to try to assign to a private class field
-	 * that was not declared in the class body. So there is absolutely no risk of unresolvable conflicts here.
-	 *
-	 * This function will modify the assignment expression passed to it if it is registered as a state field.
-	 * @param {AssignmentExpression} node
-	 * @param {Context} context
-	 */
-	register_assignment(node, context) {
-		if (
-			!(
-				node.operator === '=' &&
-				node.left.type === 'MemberExpression' &&
-				node.left.object.type === 'ThisExpression' &&
-				node.left.property.type === 'Identifier'
-			)
-		) {
-			return;
-		}
-
-		const name = ClassAnalysis.#get_name(node.left.property, this.public_state);
-		if (!name) {
-			return;
-		}
-
-		const rune = get_rune(node.right, context.state.scope);
-		if (!rune || !is_state_creation_rune(rune)) {
-			return;
-		}
-
-		const id = this.#deconflict(name);
-		const field = { kind: rune, id };
-		this.public_state.set(name, field);
-
-		// We need to do two things:
-		// - Communicate to the class body visitor that it needs to append nodes to create a state field
-		// - Modify the assignment expression so that it's valid
-		this.constructor_state_fields.push(
-			...this.build_state_field(
-				false,
-				field,
-				node.left.property,
-				// this will initialize the field without assigning to it, delegating to the constructor
-				null,
-				context
-			)
-		);
-
-		// ...swap out the assignment to go directly against the private field
-		node.left.property = id;
-		// ...and swap out the assignment's value for the state field init
-		node.right = this.#build_init_value(
-			rune,
-			/** @type {CallExpression} */ (node.right).arguments[0],
-			context
-		);
-	}
-
-	/**
-	 *
-	 * @param {PropertyDefinition | MethodDefinition | StaticBlock} node
-	 * @param {Context} context
-	 * @returns {Array<PropertyDefinition | MethodDefinition> | null}
-	 */
-	build_state_field_from_body_definition(node, context) {
-		if (
-			!(
-				node.type === 'PropertyDefinition' &&
-				(node.key.type === 'Identifier' ||
-					node.key.type === 'PrivateIdentifier' ||
-					node.key.type === 'Literal')
-			)
-		) {
-			return null;
-		}
-
-		const name = this.#definition_names.get(node.key);
-		if (!name) throw new Error('This should not be possible'); // TODO
-
-		const is_private = node.key.type === 'PrivateIdentifier';
-		const field = (is_private ? this.private_state : this.public_state).get(name);
-
-		if (!field) {
-			return null;
-		}
-
-		return this.build_state_field(
-			is_private,
-			field,
-			node.key,
-			// we can cast this because if we have a field for this definition it definitely is a call
-			// expression, otherwise it wouldn't have produced a rune earlier.
-			/** @type {CallExpression} */ (node.value),
-			context
-		);
-	}
-
-	/**
-	 * @param {boolean} is_private
-	 * @param {StateField} field
-	 * @param {(MethodDefinition | PropertyDefinition)["key"]} original_definition_id
-	 * @param {CallExpression | null} call_expression
-	 * @param {Context} context
-	 *
-	 * @returns {Array<PropertyDefinition | MethodDefinition>}
-	 */
-	build_state_field(is_private, field, original_definition_id, call_expression, context) {
-		let value;
-		if (!call_expression) {
-			// if there's no call expression, this is state that's created in the constructor.
-			// it's guaranteed to be the very first assignment to this field, so we initialize
-			// the field but don't assign to it.
-			value = null;
-		} else if (call_expression.arguments.length > 0) {
-			value = this.#build_init_value(field.kind, call_expression.arguments[0], context);
-		} else {
-			// if no arguments, we know it's state as `$derived()` is a compile error
-			value = b.call('$.state');
-		}
-
-		if (is_private) {
-			return [b.prop_def(field.id, value)];
-		}
-
-		const member = b.member(b.this, field.id);
-		const val = b.id('value');
-		return [
-			// #foo;
-			b.prop_def(field.id, value),
-			// get foo() { return this.#foo; }
-			b.method('get', original_definition_id, [], [b.return(b.call('$.get', member))]),
-			// set foo(value) { this.#foo = value; }
-			b.method(
-				'set',
-				original_definition_id,
-				[val],
-				[b.stmt(b.call('$.set', member, val, field.kind === '$state' && b.true))]
-			)
-		];
-	}
-
-	/**
-	 *
-	 * @param {StateCreationRuneName} kind
-	 * @param {Expression | SpreadElement} arg
-	 * @param {Context} context
-	 */
-	#build_init_value(kind, arg, context) {
-		const init = /** @type {Expression} **/ (
-			context.visit(arg, {
-				...context.state,
-				class_analysis: this
-			})
-		);
-
-		switch (kind) {
-			case '$state':
-				return b.call(
-					'$.state',
-					should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
-				);
-			case '$state.raw':
-				return b.call('$.state', init);
-			case '$derived':
-				return b.call('$.derived', b.thunk(init));
-			case '$derived.by':
-				return b.call('$.derived', init);
-		}
-	}
-
-	/**
-	 * @param {string} name
-	 * @returns {PrivateIdentifier}
-	 */
-	#deconflict(name) {
-		let deconflicted = name;
-		while (this.#private_ids.has(deconflicted)) {
-			deconflicted = '_' + deconflicted;
-		}
-
-		this.#private_ids.add(deconflicted);
-		return b.private_id(deconflicted);
-	}
-
-	/**
-	 * @param {Identifier | PrivateIdentifier | Literal} node
-	 * @param {Map<string, unknown>} public_state
-	 */
-	static #get_name(node, public_state) {
-		if (node.type === 'Literal') {
-			let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
-
-			// the above could generate conflicts because it has to generate a valid identifier
-			// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
-			// so we have to de-conflict. We can only check `public_state` because private state
-			// can't have literal keys
-			while (name && public_state.has(name)) {
-				name = '_' + name;
-			}
-			return name;
-		} else {
-			return node.name;
-		}
-	}
-}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js
new file mode 100644
index 000000000000..fad75b142d31
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js
@@ -0,0 +1,85 @@
+/** @import { Context } from '../../types.js' */
+/** @import { MethodDefinition, PropertyDefinition, Expression, StaticBlock, SpreadElement } from 'estree' */
+/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
+/** @import { AssignmentBuilder, ClassAnalysis, StateFieldBuilder } from '../../../shared/types.js' */
+import * as b from '#compiler/builders';
+import { create_class_analysis } from '../../../shared/class_analysis.js';
+import { should_proxy } from '../../utils.js';
+
+/**
+ * @param {Array<MethodDefinition | PropertyDefinition | StaticBlock>} body
+ * @returns {ClassAnalysis<Context>}
+ */
+export function create_client_class_analysis(body) {
+	/** @type {StateFieldBuilder<Context>} */
+	function build_state_field({ is_private, field, node, context }) {
+		let original_id = node.type === 'AssignmentExpression' ? node.left : node.key;
+		let value;
+		if (node.type === 'AssignmentExpression') {
+			// if there's no call expression, this is state that's created in the constructor.
+			// it's guaranteed to be the very first assignment to this field, so we initialize
+			// the field but don't assign to it.
+			value = null;
+		} else if (node.value.arguments.length > 0) {
+			value = build_init_value(field.kind, node.value.arguments[0], context);
+		} else {
+			// if no arguments, we know it's state as `$derived()` is a compile error
+			value = b.call('$.state');
+		}
+
+		if (is_private) {
+			return [b.prop_def(field.id, value)];
+		}
+
+		const member = b.member(b.this, field.id);
+		const val = b.id('value');
+
+		return [
+			// #foo;
+			b.prop_def(field.id, value),
+			// get foo() { return this.#foo; }
+			b.method('get', original_id, [], [b.return(b.call('$.get', member))]),
+			// set foo(value) { this.#foo = value; }
+			b.method(
+				'set',
+				original_id,
+				[val],
+				[b.stmt(b.call('$.set', member, val, field.kind === '$state' && b.true))]
+			)
+		];
+	}
+
+	/** @type {AssignmentBuilder<Context>} */
+	function build_assignment({ field, node, context }) {
+		// ...swap out the assignment to go directly against the private field
+		node.left.property = field.id;
+		// ...and swap out the assignment's value for the state field init
+		node.right = build_init_value(field.kind, node.right.arguments[0], context);
+	}
+
+	return create_class_analysis(body, build_state_field, build_assignment);
+}
+
+/**
+ *
+ * @param {StateCreationRuneName} kind
+ * @param {Expression | SpreadElement} arg
+ * @param {Context} context
+ */
+function build_init_value(kind, arg, context) {
+	const init = /** @type {Expression} **/ (context.visit(arg, context.state));
+
+	switch (kind) {
+		case '$state':
+			return b.call(
+				'$.state',
+				should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
+			);
+		case '$state.raw':
+			return b.call('$.state', init);
+		case '$derived':
+			return b.call('$.derived', b.thunk(init));
+		case '$derived.by':
+			return b.call('$.derived', init);
+	}
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index e7896991d98f..e159e2ff4e7a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -99,7 +99,7 @@ export function server_component(analysis, options) {
 		template: /** @type {any} */ (null),
 		namespace: options.namespace,
 		preserve_whitespace: options.preserveWhitespace,
-		private_derived: new Map(),
+		class_analysis: null,
 		skip_hydration_boundaries: false
 	};
 
@@ -395,7 +395,7 @@ export function server_module(analysis, options) {
 		// to be present for `javascript_visitors_legacy` and so is included in module
 		// transform state as well as component transform state
 		legacy_reactive_statements: new Map(),
-		private_derived: new Map()
+		class_analysis: null
 	};
 
 	const module = /** @type {Program} */ (
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts
index 971271642cfc..0f1b2647a288 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts
@@ -2,12 +2,12 @@ import type { Expression, Statement, ModuleDeclaration, LabeledStatement } from
 import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
 import type { TransformState } from '../types.js';
 import type { ComponentAnalysis } from '../../types.js';
-import type { StateField } from '../client/types.js';
+import type { ClassAnalysis } from '../shared/types.js';
 
 export interface ServerTransformState extends TransformState {
 	/** The $: calls, which will be ordered in the end */
 	readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
-	readonly private_derived: Map<string, StateField>;
+	readonly class_analysis: ClassAnalysis<Context> | null;
 }
 
 export interface ComponentServerTransformState extends ServerTransformState {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
index ff4a1206ddaa..8d67b3836155 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/ClassBody.js
@@ -1,9 +1,6 @@
-/** @import { ClassBody, Expression, MethodDefinition, PropertyDefinition } from 'estree' */
+/** @import { ClassBody } from 'estree' */
 /** @import { Context } from '../types.js' */
-/** @import { StateField } from '../../client/types.js' */
-import { dev } from '../../../../state.js';
-import * as b from '#compiler/builders';
-import { get_rune } from '../../../scope.js';
+import { create_server_class_analysis } from './shared/server-class-analysis.js';
 
 /**
  * @param {ClassBody} node
@@ -15,107 +12,8 @@ export function ClassBody(node, context) {
 		return;
 	}
 
-	/** @type {Map<string, StateField>} */
-	const public_derived = new Map();
-
-	/** @type {Map<string, StateField>} */
-	const private_derived = new Map();
-
-	/** @type {string[]} */
-	const private_ids = [];
-
-	for (const definition of node.body) {
-		if (
-			definition.type === 'PropertyDefinition' &&
-			(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
-		) {
-			const { type, name } = definition.key;
-
-			const is_private = type === 'PrivateIdentifier';
-			if (is_private) private_ids.push(name);
-
-			if (definition.value?.type === 'CallExpression') {
-				const rune = get_rune(definition.value, context.state.scope);
-				if (rune === '$derived' || rune === '$derived.by') {
-					/** @type {StateField} */
-					const field = {
-						kind: rune,
-						// @ts-expect-error this is set in the next pass
-						id: is_private ? definition.key : null
-					};
-
-					if (is_private) {
-						private_derived.set(name, field);
-					} else {
-						public_derived.set(name, field);
-					}
-				}
-			}
-		}
-	}
-
-	// each `foo = $derived()` needs a backing `#foo` field
-	for (const [name, field] of public_derived) {
-		let deconflicted = name;
-		while (private_ids.includes(deconflicted)) {
-			deconflicted = '_' + deconflicted;
-		}
-
-		private_ids.push(deconflicted);
-		field.id = b.private_id(deconflicted);
-	}
-
-	/** @type {Array<MethodDefinition | PropertyDefinition>} */
-	const body = [];
-
-	const child_state = { ...context.state, private_derived };
-
-	// Replace parts of the class body
-	for (const definition of node.body) {
-		if (
-			definition.type === 'PropertyDefinition' &&
-			(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
-		) {
-			const name = definition.key.name;
-
-			const is_private = definition.key.type === 'PrivateIdentifier';
-			const field = (is_private ? private_derived : public_derived).get(name);
-
-			if (definition.value?.type === 'CallExpression' && field !== undefined) {
-				const init = /** @type {Expression} **/ (
-					context.visit(definition.value.arguments[0], child_state)
-				);
-				const value =
-					field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
-
-				if (is_private) {
-					body.push(b.prop_def(field.id, value));
-				} else {
-					// #foo;
-					const member = b.member(b.this, field.id);
-					body.push(b.prop_def(field.id, value));
-
-					// get foo() { return this.#foo; }
-					body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
-
-					if (dev && (field.kind === '$derived' || field.kind === '$derived.by')) {
-						body.push(
-							b.method(
-								'set',
-								definition.key,
-								[b.id('_')],
-								[b.throw_error(`Cannot update a derived property ('${name}')`)]
-							)
-						);
-					}
-				}
-
-				continue;
-			}
-		}
-
-		body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
-	}
+	const class_analysis = create_server_class_analysis(node.body);
+	const body = class_analysis.generate_body(context);
 
 	return { ...node, body };
 }
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
index 73631395e66e..2f72327191d0 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/MemberExpression.js
@@ -12,7 +12,10 @@ export function MemberExpression(node, context) {
 		node.object.type === 'ThisExpression' &&
 		node.property.type === 'PrivateIdentifier'
 	) {
-		const field = context.state.private_derived.get(node.property.name);
+		const field = context.state.class_analysis?.get_field(node.property.name, true, [
+			'$derived',
+			'$derived.by'
+		]);
 
 		if (field) {
 			return b.call(node);
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js
new file mode 100644
index 000000000000..a94972ed93d9
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js
@@ -0,0 +1,71 @@
+/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition } from 'estree' */
+/** @import { Context } from '../../types.js' */
+/** @import { AssignmentBuilder, StateFieldBuilder } from '../../../shared/types.js' */
+/** @import { ClassAnalysis } from '../../../shared/types.js' */
+
+import * as b from '#compiler/builders';
+import { dev } from '../../../../../state.js';
+import { create_class_analysis } from '../../../shared/class_analysis.js';
+
+/**
+ * @param {Array<MethodDefinition | PropertyDefinition | StaticBlock>} body
+ * @returns {ClassAnalysis<Context>}
+ */
+export function create_server_class_analysis(body) {
+	/** @type {StateFieldBuilder<Context>} */
+	function build_state_field({ is_private, field, node, context }) {
+		let original_id = node.type === 'AssignmentExpression' ? node.left : node.key;
+		let value;
+		if (node.type === 'AssignmentExpression') {
+			// This means it's a state assignment in the constructor (this.foo = $state('bar'))
+			// which means the state field needs to have no default value so that the initial
+			// value can be assigned in the constructor.
+			value = null;
+		} else if (field.kind !== '$derived' && field.kind !== '$derived.by') {
+			return [/** @type {PropertyDefinition} */ (context.visit(node, context.state))];
+		} else {
+			const init = /** @type {Expression} **/ (
+				context.visit(node.value.arguments[0], context.state)
+			);
+			value =
+				field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
+		}
+
+		if (is_private) {
+			return [b.prop_def(field.id, value)];
+		}
+		// #foo;
+		const member = b.member(b.this, field.id);
+
+		const defs = [
+			// #foo;
+			b.prop_def(field.id, value),
+			// get foo() { return this.#foo; }
+			b.method('get', original_id, [], [b.return(b.call(member))])
+		];
+
+		// TODO make this work on server
+		if (dev) {
+			defs.push(
+				b.method(
+					'set',
+					original_id,
+					[b.id('_')],
+					[b.throw_error(`Cannot update a derived property ('${name}')`)]
+				)
+			);
+		}
+
+		return defs;
+	}
+
+	/** @type {AssignmentBuilder<Context>} */
+	function build_assignment({ field, node, context }) {
+		node.left.property = field.id;
+		const init = /** @type {Expression} **/ (context.visit(node.right.arguments[0], context.state));
+		node.right =
+			field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
+	}
+
+	return create_class_analysis(body, build_state_field, build_assignment);
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js b/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js
new file mode 100644
index 000000000000..5eb001d438b8
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js
@@ -0,0 +1,334 @@
+/** @import { AssignmentExpression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, StaticBlock } from 'estree' */
+/** @import { StateField } from '../types.js' */
+/** @import { Context as ClientContext } from '../client/types.js' */
+/** @import { Context as ServerContext } from '../server/types.js' */
+/** @import { StateCreationRuneName } from '../../../../utils.js' */
+/** @import { AssignmentBuilder, ClassAnalysis, StateFieldBuilder, StatefulAssignment, StatefulPropertyDefinition } from './types.js' */
+/** @import { Scope }  from '../../scope.js' */
+import * as b from '#compiler/builders';
+import { once } from '../../../../internal/server/index.js';
+import { is_state_creation_rune, STATE_CREATION_RUNES } from '../../../../utils.js';
+import { regex_invalid_identifier_chars } from '../../patterns.js';
+import { get_rune } from '../../scope.js';
+
+/**
+ * @template {ClientContext | ServerContext} TContext
+ * @param {Array<PropertyDefinition | MethodDefinition | StaticBlock>} body
+ * @param {StateFieldBuilder<TContext>} build_state_field
+ * @param {AssignmentBuilder<TContext>} build_assignment
+ * @returns {ClassAnalysis<TContext>}
+ */
+export function create_class_analysis(body, build_state_field, build_assignment) {
+	/** @type {Map<string, StateField>} */
+	const public_fields = new Map();
+
+	/** @type {Map<string, StateField>} */
+	const private_fields = new Map();
+
+	/** @type {Array<PropertyDefinition | MethodDefinition>} */
+	const new_body = [];
+
+	/**
+	 * Private identifiers in use by this analysis.
+	 * Factoid: Unlike public class fields, private fields _must_ be declared in the class body
+	 * before use. So the following is actually a JavaScript syntax error, which means we can
+	 * be 100% certain we know all private fields after parsing the class body:
+	 *
+	 * ```ts
+	 * class Example {
+	 *   constructor() {
+	 *     this.public = 'foo'; // not a problem!
+	 *     this.#private = 'bar'; // JavaScript parser error
+	 *   }
+	 * }
+	 * ```
+	 * @type {Set<string>}
+	 */
+	const private_ids = new Set();
+
+	/**
+	 * A registry of functions to call to complete body modifications.
+	 * Replacements may insert more than one node to the body. The original
+	 * body should not be modified -- instead, replacers should push new
+	 * nodes to new_body.
+	 *
+	 * @type {Array<() => void>}
+	 */
+	const replacers = [];
+
+	/**
+	 * @param {string} name
+	 * @param {boolean} is_private
+	 * @param {ReadonlyArray<StateCreationRuneName>} [kinds]
+	 */
+	function get_field(name, is_private, kinds = STATE_CREATION_RUNES) {
+		const value = (is_private ? private_fields : public_fields).get(name);
+		if (value && kinds.includes(value.kind)) {
+			return value;
+		}
+	}
+
+	/**
+	 * @param {TContext} context
+	 * @returns {TContext}
+	 */
+	function create_child_context(context) {
+		return {
+			...context,
+			state: {
+				...context.state,
+				class_analysis
+			}
+		};
+	}
+
+	/**
+	 * @param {TContext} context
+	 */
+	function generate_body(context) {
+		const child_context = create_child_context(context);
+		for (const node of body) {
+			const was_registered = register_body_definition(node, child_context);
+			if (!was_registered) {
+				new_body.push(
+					/** @type {PropertyDefinition | MethodDefinition} */ (
+						// @ts-expect-error generics silliness
+						child_context.visit(node, child_context.state)
+					)
+				);
+			}
+		}
+
+		for (const replacer of replacers) {
+			replacer();
+		}
+
+		return new_body;
+	}
+
+	/**
+	 * Important note: It is a syntax error in JavaScript to try to assign to a private class field
+	 * that was not declared in the class body. So there is absolutely no risk of unresolvable conflicts here.
+	 *
+	 * This function will modify the assignment expression passed to it if it is registered as a state field.
+	 * @param {AssignmentExpression} node
+	 * @param {TContext} context
+	 */
+	function register_assignment(node, context) {
+		const child_context = create_child_context(context);
+		if (
+			!(
+				node.operator === '=' &&
+				node.left.type === 'MemberExpression' &&
+				node.left.object.type === 'ThisExpression' &&
+				node.left.property.type === 'Identifier'
+			)
+		) {
+			return;
+		}
+
+		const name = get_name(node.left.property);
+		if (!name) {
+			return;
+		}
+
+		const parsed = parse_stateful_assignment(node, child_context.state.scope);
+		if (!parsed) {
+			return;
+		}
+		const { stateful_assignment, rune } = parsed;
+
+		const id = deconflict(name);
+		const field = { kind: rune, id };
+		public_fields.set(name, field);
+
+		const replacer = () => {
+			const nodes = build_state_field({
+				is_private: false,
+				field,
+				node: stateful_assignment,
+				context: child_context
+			});
+			if (!nodes) {
+				return;
+			}
+			new_body.push(...nodes);
+		};
+		replacers.push(replacer);
+
+		build_assignment({
+			node: stateful_assignment,
+			field,
+			context: child_context
+		});
+	}
+
+	/**
+	 * @param {PropertyDefinition | MethodDefinition | StaticBlock} node
+	 * @param {TContext} child_context
+	 * @returns {boolean} if this node is stateful and was registered
+	 */
+	function register_body_definition(node, child_context) {
+		if (node.type === 'MethodDefinition' && node.kind === 'constructor') {
+			// life is easier to reason about if we've visited the constructor
+			// and registered its public state field before we start building
+			// anything else
+			replacers.unshift(() => {
+				new_body.push(
+					/** @type {MethodDefinition} */ (
+						// @ts-expect-error generics silliness
+						child_context.visit(node, child_context.state)
+					)
+				);
+			});
+			return true;
+		}
+
+		if (
+			!(
+				(node.type === 'PropertyDefinition' || node.type === 'MethodDefinition') &&
+				(node.key.type === 'Identifier' ||
+					node.key.type === 'PrivateIdentifier' ||
+					node.key.type === 'Literal')
+			)
+		) {
+			return false;
+		}
+
+		/*
+		 * We don't know if the node is stateful yet, but we still need to register some details.
+		 * For example: If the node is a private identifier, we could accidentally conflict with it later
+		 * if we create a private field for public state (as would happen in this example:)
+		 *
+		 * ```ts
+		 * class Foo {
+		 *   #count = 0;
+		 *   count = $state(0); // would become #count if we didn't know about the private field above
+		 * }
+		 */
+
+		const name = get_name(node.key);
+		if (!name) {
+			return false;
+		}
+
+		const is_private = node.key.type === 'PrivateIdentifier';
+		if (is_private) {
+			private_ids.add(name);
+		}
+
+		const parsed = prop_def_is_stateful(node, child_context.state.scope);
+		if (!parsed) {
+			return false;
+		}
+		const { stateful_prop_def, rune } = parsed;
+
+		let field;
+		if (is_private) {
+			field = {
+				kind: rune,
+				id: /** @type {PrivateIdentifier} */ (stateful_prop_def.key)
+			};
+			private_fields.set(name, field);
+		} else {
+			// We can't set the ID until we've identified all of the private state fields,
+			// otherwise we might conflict with them. After registering all property definitions,
+			// call `finalize_property_definitions` to populate the IDs. So long as we don't
+			// access the ID before the end of this loop, we're fine!
+			const id = once(() => deconflict(name));
+			field = {
+				kind: rune,
+				get id() {
+					return id();
+				}
+			};
+			public_fields.set(name, field);
+		}
+
+		const replacer = () => {
+			const nodes = build_state_field({
+				is_private,
+				field,
+				node: stateful_prop_def,
+				context: child_context
+			});
+			if (!nodes) {
+				return;
+			}
+			new_body.push(...nodes);
+		};
+		replacers.push(replacer);
+
+		return true;
+	}
+
+	/**
+	 * @param {string} name
+	 * @returns {PrivateIdentifier}
+	 */
+	function deconflict(name) {
+		let deconflicted = name;
+		while (private_ids.has(deconflicted)) {
+			deconflicted = '_' + deconflicted;
+		}
+
+		private_ids.add(deconflicted);
+		return b.private_id(deconflicted);
+	}
+
+	/**
+	 * @param {Identifier | PrivateIdentifier | Literal} node
+	 */
+	function get_name(node) {
+		if (node.type === 'Literal') {
+			let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_');
+
+			// the above could generate conflicts because it has to generate a valid identifier
+			// so stuff like `0` and `1` or `state%` and `state^` will result in the same string
+			// so we have to de-conflict. We can only check `public_fields` because private state
+			// can't have literal keys
+			while (name && public_fields.has(name)) {
+				name = '_' + name;
+			}
+			return name;
+		} else {
+			return node.name;
+		}
+	}
+
+	const class_analysis = {
+		get_field,
+		generate_body,
+		register_assignment
+	};
+
+	return class_analysis;
+}
+
+/**
+ * `get_rune` is really annoying because it really guarantees this already
+ * we just need this to tell the type system about it
+ * @param {AssignmentExpression} node
+ * @param {Scope} scope
+ * @returns {{ stateful_assignment: StatefulAssignment, rune: StateCreationRuneName } | null}
+ */
+function parse_stateful_assignment(node, scope) {
+	const rune = get_rune(node.right, scope);
+	if (!rune || !is_state_creation_rune(rune)) {
+		return null;
+	}
+	return { stateful_assignment: /** @type {StatefulAssignment} */ (node), rune };
+}
+
+/**
+ * @param {PropertyDefinition | MethodDefinition} node
+ * @param {Scope} scope
+ * @returns {{ stateful_prop_def: StatefulPropertyDefinition, rune: StateCreationRuneName } | null}
+ */
+function prop_def_is_stateful(node, scope) {
+	const rune = get_rune(node.value, scope);
+	if (!rune || !is_state_creation_rune(rune)) {
+		return null;
+	}
+	return { stateful_prop_def: /** @type {StatefulPropertyDefinition} */ (node), rune };
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts
new file mode 100644
index 000000000000..45bd3b5a00ae
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts
@@ -0,0 +1,63 @@
+import type { AssignmentExpression, CallExpression, Identifier, MemberExpression, PropertyDefinition, MethodDefinition, PrivateIdentifier, ThisExpression } from 'estree';
+import type { StateField } from '../types';
+import type { Context as ServerContext } from '../server/types';
+import type { Context as ClientContext } from '../client/types';
+import type { StateCreationRuneName } from '../../../../utils';
+
+export type StatefulAssignment = AssignmentExpression & {
+	left: MemberExpression & {
+		object: ThisExpression;
+		property: Identifier | PrivateIdentifier
+	};
+	right: CallExpression;
+};
+
+export type StatefulPropertyDefinition = PropertyDefinition & {
+	key: Identifier | PrivateIdentifier;
+	value: CallExpression;
+};
+
+export type StateFieldBuilderParams<TContext extends ServerContext | ClientContext> = {
+	is_private: boolean;
+	field: StateField;
+	node: StatefulAssignment | StatefulPropertyDefinition;
+	context: TContext;
+};
+
+export type StateFieldBuilder<TContext extends ServerContext | ClientContext> = (
+	params: StateFieldBuilderParams<TContext>
+) => Array<PropertyDefinition | MethodDefinition>;
+
+export type AssignmentBuilderParams<TContext extends ServerContext | ClientContext> = {
+	node: StatefulAssignment;
+	field: StateField;
+	context: TContext;
+};
+
+export type AssignmentBuilder<TContext  extends ServerContext | ClientContext> = (params: AssignmentBuilderParams<TContext>) => void;
+
+export type ClassAnalysis<TContext extends ServerContext | ClientContext> = {
+	/**
+	 * @param name - The name of the field.
+	 * @param is_private - Whether the field is private (whether its name starts with '#').
+	 * @param kinds - What kinds of state creation runes you're looking for, eg. only '$derived.by'.
+	 * @returns The field if it exists and matches the given criteria, or null.
+	 */
+	get_field: (name: string, is_private: boolean, kinds?: Array<StateCreationRuneName>) => StateField | undefined;
+
+	/**
+	 * Given the body of a class, generate a new body with stateful fields.
+	 * This assumes that {@link register_assignment} is registered to be called
+	 * for all `AssignmentExpression` nodes in the class body.
+	 * @param context - The context associated with the `ClassBody`.
+	 * @returns The new body.
+	 */
+	generate_body: (context: TContext) => Array<PropertyDefinition | MethodDefinition>;
+
+	/**
+	 * Register an assignment expression. This checks to see if the assignment is creating
+	 * a state field on the class. If it is, it registers that state field and modifies the
+	 * assignment expression.
+	 */
+	register_assignment: (node: AssignmentExpression, context: TContext) => void;
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/types.d.ts
index 5d860207dde6..8999f096142e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/types.d.ts
@@ -1,6 +1,8 @@
 import type { Scope } from '../scope.js';
 import type { AST, ValidatedModuleCompileOptions } from '#compiler';
 import type { Analysis } from '../types.js';
+import type { StateCreationRuneName } from '../../../utils.js';
+import type { PrivateIdentifier } from 'estree';
 
 export interface TransformState {
 	readonly analysis: Analysis;
@@ -8,3 +10,8 @@ export interface TransformState {
 	readonly scope: Scope;
 	readonly scopes: Map<AST.SvelteNode, Scope>;
 }
+
+export interface StateField {
+	kind: StateCreationRuneName;
+	id: PrivateIdentifier;
+}
\ No newline at end of file
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index 2758b9fcbc19..654b4bb2d0c1 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -428,15 +428,18 @@ export function is_mathml(name) {
 	return MATHML_ELEMENTS.includes(name);
 }
 
-const RUNES = /** @type {const} */ ([
+export const STATE_CREATION_RUNES = /** @type {const} */ ([
 	'$state',
 	'$state.raw',
+	'$derived',
+	'$derived.by'
+]);
+const RUNES = /** @type {const} */ ([
+	...STATE_CREATION_RUNES,
 	'$state.snapshot',
 	'$props',
 	'$props.id',
 	'$bindable',
-	'$derived',
-	'$derived.by',
 	'$effect',
 	'$effect.pre',
 	'$effect.tracking',
@@ -457,7 +460,7 @@ export function is_rune(name) {
 	return RUNES.includes(/** @type {RUNES[number]} */ (name));
 }
 
-/** @typedef {'$state' | '$state.raw' | '$derived' | '$derived.by'} StateCreationRuneName */
+/** @typedef {STATE_CREATION_RUNES[number]} StateCreationRuneName */
 
 /**
  * @param {string} name
diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js
index 21339741761f..620623ebfd7d 100644
--- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js
@@ -5,6 +5,11 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
 	$.push($$props, true);
 
 	class Foo {
+		constructor() {
+			this.a = 1;
+			$.set(this.#b, 2);
+		}
+
 		#a = $.state();
 
 		get a() {
@@ -16,11 +21,6 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
 		}
 
 		#b = $.state();
-
-		constructor() {
-			this.a = 1;
-			$.set(this.#b, 2);
-		}
 	}
 
 	$.pop();
diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js
index 2a115a498373..a27bcb8623a7 100644
--- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js
@@ -4,13 +4,13 @@ export default function Class_state_field_constructor_assignment($$payload, $$pr
 	$.push();
 
 	class Foo {
-		a;
-		#b;
-
 		constructor() {
 			this.a = 1;
 			this.#b = 2;
 		}
+
+		a;
+		#b;
 	}
 
 	$.pop();

From 92940ffbfdb46dd7a1edb25fd361bc3a3c2092f8 Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Tue, 29 Apr 2025 12:15:08 -0400
Subject: [PATCH 07/10] feat: It works I think?

---
 .../client/visitors/AssignmentExpression.js   |  6 +-
 .../visitors/shared/client-class-analysis.js  | 20 +++++--
 .../server/visitors/AssignmentExpression.js   |  5 ++
 .../visitors/shared/server-class-analysis.js  | 51 ++++++++++++++---
 .../3-transform/shared/class_analysis.js      | 56 ++++++++++++++-----
 .../phases/3-transform/shared/types.d.ts      |  4 +-
 .../_config.js                                |  0
 .../main.svelte                               |  0
 .../_config.js                                | 13 +++++
 .../main.svelte                               | 13 +++++
 .../_config.js                                | 45 +++++++++++++++
 .../main.svelte                               | 37 ++++++++++++
 .../_config.js                                | 20 +++++++
 .../main.svelte                               | 22 ++++++++
 .../class-state-constructor/_config.js        | 20 +++++++
 .../class-state-constructor/main.svelte       | 18 ++++++
 .../class-state-constructor-1/errors.json     | 14 +++++
 .../class-state-constructor-1/input.svelte.js |  7 +++
 .../class-state-constructor-2/errors.json     | 14 +++++
 .../class-state-constructor-2/input.svelte.js |  7 +++
 .../class-state-constructor-3/errors.json     | 14 +++++
 .../class-state-constructor-3/input.svelte.js |  7 +++
 .../class-state-constructor-4/errors.json     | 14 +++++
 .../class-state-constructor-4/input.svelte.js |  7 +++
 24 files changed, 381 insertions(+), 33 deletions(-)
 rename packages/svelte/tests/runtime-runes/samples/{class-state-constructor-closure-private => class-state-constructor-closure-private-1}/_config.js (100%)
 rename packages/svelte/tests/runtime-runes/samples/{class-state-constructor-closure-private => class-state-constructor-closure-private-1}/main.svelte (100%)
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js

diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
index 7d0840ab8098..ff8d2b5ce12c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
@@ -17,7 +17,11 @@ import { validate_mutation } from './shared/utils.js';
  * @param {Context} context
  */
 export function AssignmentExpression(node, context) {
-	context.state.class_analysis?.register_assignment(node, context);
+	const stripped_node = context.state.class_analysis?.register_assignment(node, context);
+	if (stripped_node) {
+		return stripped_node;
+	}
+
 	const expression = /** @type {Expression} */ (
 		visit_assignment_expression(node, context, build_assignment) ?? context.next()
 	);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js
index fad75b142d31..70dd3646765b 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js
@@ -13,7 +13,7 @@ import { should_proxy } from '../../utils.js';
 export function create_client_class_analysis(body) {
 	/** @type {StateFieldBuilder<Context>} */
 	function build_state_field({ is_private, field, node, context }) {
-		let original_id = node.type === 'AssignmentExpression' ? node.left : node.key;
+		let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key;
 		let value;
 		if (node.type === 'AssignmentExpression') {
 			// if there's no call expression, this is state that's created in the constructor.
@@ -51,10 +51,16 @@ export function create_client_class_analysis(body) {
 
 	/** @type {AssignmentBuilder<Context>} */
 	function build_assignment({ field, node, context }) {
-		// ...swap out the assignment to go directly against the private field
-		node.left.property = field.id;
-		// ...and swap out the assignment's value for the state field init
-		node.right = build_init_value(field.kind, node.right.arguments[0], context);
+		return {
+			...node,
+			left: {
+				...node.left,
+				// ...swap out the assignment to go directly against the private field
+				property: field.id
+			},
+			// ...and swap out the assignment's value for the state field init
+			right: build_init_value(field.kind, node.right.arguments[0], context)
+		};
 	}
 
 	return create_class_analysis(body, build_state_field, build_assignment);
@@ -67,7 +73,9 @@ export function create_client_class_analysis(body) {
  * @param {Context} context
  */
 function build_init_value(kind, arg, context) {
-	const init = /** @type {Expression} **/ (context.visit(arg, context.state));
+	const init = /** @type {Expression} **/ (
+		context.visit(arg, { ...context.state, in_constructor: false })
+	);
 
 	switch (kind) {
 		case '$state':
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
index 071a12f9bcdd..9977c979db3f 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
@@ -10,6 +10,11 @@ import { visit_assignment_expression } from '../../shared/assignments.js';
  * @param {Context} context
  */
 export function AssignmentExpression(node, context) {
+	const stripped_node = context.state.class_analysis?.register_assignment(node, context);
+	if (stripped_node) {
+		return stripped_node;
+	}
+
 	return visit_assignment_expression(node, context, build_assignment) ?? context.next();
 }
 
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js
index a94972ed93d9..44a641508766 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js
@@ -1,7 +1,8 @@
-/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition } from 'estree' */
+/** @import { Expression, MethodDefinition, StaticBlock, PropertyDefinition, SpreadElement } from 'estree' */
 /** @import { Context } from '../../types.js' */
 /** @import { AssignmentBuilder, StateFieldBuilder } from '../../../shared/types.js' */
 /** @import { ClassAnalysis } from '../../../shared/types.js' */
+/** @import { StateCreationRuneName } from '../../../../../../utils.js' */
 
 import * as b from '#compiler/builders';
 import { dev } from '../../../../../state.js';
@@ -14,7 +15,7 @@ import { create_class_analysis } from '../../../shared/class_analysis.js';
 export function create_server_class_analysis(body) {
 	/** @type {StateFieldBuilder<Context>} */
 	function build_state_field({ is_private, field, node, context }) {
-		let original_id = node.type === 'AssignmentExpression' ? node.left : node.key;
+		let original_id = node.type === 'AssignmentExpression' ? node.left.property : node.key;
 		let value;
 		if (node.type === 'AssignmentExpression') {
 			// This means it's a state assignment in the constructor (this.foo = $state('bar'))
@@ -37,13 +38,19 @@ export function create_server_class_analysis(body) {
 		// #foo;
 		const member = b.member(b.this, field.id);
 
+		/** @type {Array<MethodDefinition | PropertyDefinition>} */
 		const defs = [
 			// #foo;
-			b.prop_def(field.id, value),
-			// get foo() { return this.#foo; }
-			b.method('get', original_id, [], [b.return(b.call(member))])
+			b.prop_def(field.id, value)
 		];
 
+		// get foo() { return this.#foo; }
+		if (field.kind === '$state' || field.kind === '$state.raw') {
+			defs.push(b.method('get', original_id, [], [b.return(member)]));
+		} else {
+			defs.push(b.method('get', original_id, [], [b.return(b.call(member))]));
+		}
+
 		// TODO make this work on server
 		if (dev) {
 			defs.push(
@@ -61,11 +68,37 @@ export function create_server_class_analysis(body) {
 
 	/** @type {AssignmentBuilder<Context>} */
 	function build_assignment({ field, node, context }) {
-		node.left.property = field.id;
-		const init = /** @type {Expression} **/ (context.visit(node.right.arguments[0], context.state));
-		node.right =
-			field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
+		return {
+			...node,
+			left: {
+				...node.left,
+				// ...swap out the assignment to go directly against the private field
+				property: field.id
+			},
+			// ...and swap out the assignment's value for the state field init
+			right: build_init_value(field.kind, node.right.arguments[0], context)
+		};
 	}
 
 	return create_class_analysis(body, build_state_field, build_assignment);
 }
+
+/**
+ *
+ * @param {StateCreationRuneName} kind
+ * @param {Expression | SpreadElement} arg
+ * @param {Context} context
+ */
+function build_init_value(kind, arg, context) {
+	const init = /** @type {Expression} **/ (context.visit(arg, context.state));
+
+	switch (kind) {
+		case '$state':
+		case '$state.raw':
+			return init;
+		case '$derived':
+			return b.call('$.once', b.thunk(init));
+		case '$derived.by':
+			return b.call('$.once', init);
+	}
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js b/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js
index 5eb001d438b8..e482f10d49dc 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js
@@ -107,12 +107,9 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 	}
 
 	/**
-	 * Important note: It is a syntax error in JavaScript to try to assign to a private class field
-	 * that was not declared in the class body. So there is absolutely no risk of unresolvable conflicts here.
-	 *
-	 * This function will modify the assignment expression passed to it if it is registered as a state field.
 	 * @param {AssignmentExpression} node
 	 * @param {TContext} context
+	 * @returns {AssignmentExpression | null} The node, if `register_assignment` handled its transformation.
 	 */
 	function register_assignment(node, context) {
 		const child_context = create_child_context(context);
@@ -121,30 +118,46 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 				node.operator === '=' &&
 				node.left.type === 'MemberExpression' &&
 				node.left.object.type === 'ThisExpression' &&
-				node.left.property.type === 'Identifier'
+				(node.left.property.type === 'Identifier' ||
+					node.left.property.type === 'PrivateIdentifier')
 			)
 		) {
-			return;
+			return null;
 		}
 
 		const name = get_name(node.left.property);
 		if (!name) {
-			return;
+			return null;
 		}
 
 		const parsed = parse_stateful_assignment(node, child_context.state.scope);
 		if (!parsed) {
-			return;
+			return null;
 		}
 		const { stateful_assignment, rune } = parsed;
 
-		const id = deconflict(name);
-		const field = { kind: rune, id };
-		public_fields.set(name, field);
+		const is_private = stateful_assignment.left.property.type === 'PrivateIdentifier';
+
+		let field;
+		if (is_private) {
+			field = {
+				kind: rune,
+				id: /** @type {PrivateIdentifier} */ (stateful_assignment.left.property)
+			};
+			private_fields.set(name, field);
+		} else {
+			field = {
+				kind: rune,
+				// it's safe to do this upfront now because we're guaranteed to already know about all private
+				// identifiers (they had to have been declared at the class root, before we visited the constructor)
+				id: deconflict(name)
+			};
+			public_fields.set(name, field);
+		}
 
 		const replacer = () => {
 			const nodes = build_state_field({
-				is_private: false,
+				is_private,
 				field,
 				node: stateful_assignment,
 				context: child_context
@@ -156,9 +169,9 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 		};
 		replacers.push(replacer);
 
-		build_assignment({
-			node: stateful_assignment,
+		return build_assignment({
 			field,
+			node: stateful_assignment,
 			context: child_context
 		});
 	}
@@ -219,7 +232,20 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 
 		const parsed = prop_def_is_stateful(node, child_context.state.scope);
 		if (!parsed) {
-			return false;
+			// this isn't a stateful field definition, but if could become one in the constructor -- so we register
+			// it, but conditionally -- so that if it's added as a field in the constructor (which causes us to create)
+			// a field definition for it), we don't end up with a duplicate definition (this one, plus the one we create)
+			replacers.push(() => {
+				if (!get_field(name, is_private)) {
+					new_body.push(
+						/** @type {PropertyDefinition | MethodDefinition} */ (
+							// @ts-expect-error generics silliness
+							child_context.visit(node, child_context.state)
+						)
+					);
+				}
+			});
+			return true;
 		}
 		const { stateful_prop_def, rune } = parsed;
 
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts
index 45bd3b5a00ae..96fe27ca5dcc 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts
@@ -34,7 +34,7 @@ export type AssignmentBuilderParams<TContext extends ServerContext | ClientConte
 	context: TContext;
 };
 
-export type AssignmentBuilder<TContext  extends ServerContext | ClientContext> = (params: AssignmentBuilderParams<TContext>) => void;
+export type AssignmentBuilder<TContext  extends ServerContext | ClientContext> = (params: AssignmentBuilderParams<TContext>) => AssignmentExpression;
 
 export type ClassAnalysis<TContext extends ServerContext | ClientContext> = {
 	/**
@@ -59,5 +59,5 @@ export type ClassAnalysis<TContext extends ServerContext | ClientContext> = {
 	 * a state field on the class. If it is, it registers that state field and modifies the
 	 * assignment expression.
 	 */
-	register_assignment: (node: AssignmentExpression, context: TContext) => void;
+	register_assignment: (node: AssignmentExpression, context: TContext) => AssignmentExpression | null;
 }
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/_config.js
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/_config.js
rename to packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/_config.js
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/main.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private/main.svelte
rename to packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-1/main.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js
new file mode 100644
index 000000000000..dd847ce2f2a6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/_config.js
@@ -0,0 +1,13 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	html: `<button>10</button>`,
+	ssrHtml: `<button>0</button>`,
+
+	async test({ assert, target }) {
+		flushSync();
+
+		assert.htmlEqual(target.innerHTML, `<button>10</button>`);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte
new file mode 100644
index 000000000000..3d8ea414187e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-2/main.svelte
@@ -0,0 +1,13 @@
+<script>
+	class Counter {
+		constructor() {
+			this.count = $state(0);
+			$effect(() => {
+				this.count = 10;
+			});
+		}
+	}
+	const counter = new Counter();
+</script>
+
+<button on:click={() => counter.count++}>{counter.count}</button>
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js
new file mode 100644
index 000000000000..4cf1aea213dc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js
@@ -0,0 +1,45 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	// The component context class instance gets shared between tests, strangely, causing hydration to fail?
+	mode: ['client', 'server'],
+
+	async test({ assert, target, logs }) {
+		const btn = target.querySelector('button');
+
+		flushSync(() => {
+			btn?.click();
+		});
+
+		assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1]);
+
+		flushSync(() => {
+			btn?.click();
+		});
+
+		assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2]);
+
+		flushSync(() => {
+			btn?.click();
+		});
+
+		assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2, 3]);
+
+		flushSync(() => {
+			btn?.click();
+		});
+
+		assert.deepEqual(logs, [
+			0,
+			'class trigger false',
+			'local trigger false',
+			1,
+			2,
+			3,
+			4,
+			'class trigger true',
+			'local trigger true'
+		]);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte
new file mode 100644
index 000000000000..03687d01bb3d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte
@@ -0,0 +1,37 @@
+<script module>
+	class SomeLogic {
+		trigger() {
+			this.someValue++;
+		}
+
+		constructor() {
+			this.someValue = $state(0);
+			this.isAboveThree = $derived(this.someValue > 3);
+		}
+	}
+
+	const someLogic = new SomeLogic();
+</script>
+
+<script>
+	function increment() {
+		someLogic.trigger();
+	}
+
+	let localDerived = $derived(someLogic.someValue > 3);
+
+	$effect(() => {
+		console.log(someLogic.someValue);
+	});
+	$effect(() => {
+		console.log('class trigger ' + someLogic.isAboveThree)
+	});
+	$effect(() => {
+		console.log('local trigger ' + localDerived)
+	});
+
+</script>
+
+<button on:click={increment}>
+	clicks: {someLogic.someValue}
+</button>
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js
new file mode 100644
index 000000000000..32cca6c69375
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	html: `<button>10: 20</button>`,
+
+	test({ assert, target }) {
+		const btn = target.querySelector('button');
+
+		flushSync(() => {
+			btn?.click();
+		});
+		assert.htmlEqual(target.innerHTML, `<button>11: 22</button>`);
+
+		flushSync(() => {
+			btn?.click();
+		});
+		assert.htmlEqual(target.innerHTML, `<button>12: 24</button>`);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte
new file mode 100644
index 000000000000..d8feb554cd18
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte
@@ -0,0 +1,22 @@
+<script>
+	class Counter {
+		constructor(initial) {
+			this.count = $state(initial);
+		}
+
+		increment = () => {
+			this.count++;
+		}
+	}
+
+	class PluggableCounter extends Counter {
+		constructor(initial, plugin) {
+			super(initial)
+			this.custom = $derived(plugin(this.count));
+		}
+	}
+
+	const counter = new PluggableCounter(10, (count) => count * 2);
+</script>
+
+<button onclick={counter.increment}>{counter.count}: {counter.custom}</button>
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js
new file mode 100644
index 000000000000..f35dc57228a1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	html: `<button>20</button>`,
+
+	test({ assert, target }) {
+		const btn = target.querySelector('button');
+
+		flushSync(() => {
+			btn?.click();
+		});
+		assert.htmlEqual(target.innerHTML, `<button>22</button>`);
+
+		flushSync(() => {
+			btn?.click();
+		});
+		assert.htmlEqual(target.innerHTML, `<button>24</button>`);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte
new file mode 100644
index 000000000000..aa8ba1658b03
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte
@@ -0,0 +1,18 @@
+<script>
+	class Counter {
+		/** @type {number} */
+		#count;
+
+		constructor(initial) {
+			this.#count = $state(initial);
+			this.doubled = $derived(this.#count * 2);
+		}
+
+		increment = () => {
+			this.#count++;
+		}
+	}
+	const counter = new Counter(10);
+</script>
+
+<button onclick={counter.increment}>{counter.doubled}</button>
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json
new file mode 100644
index 000000000000..eaff12b89645
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json
@@ -0,0 +1,14 @@
+[
+	{
+		"code": "constructor_state_reassignment",
+		"message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):2:1`",
+		"start": {
+			"line": 5,
+			"column": 2
+		},
+		"end": {
+			"line": 5,
+			"column": 24
+		}
+	}
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js
new file mode 100644
index 000000000000..05cd4d9d9d64
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+	count = $state(0);
+
+	constructor() {
+		this.count = $state(0);
+	}
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json
new file mode 100644
index 000000000000..a27d7411d1f6
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json
@@ -0,0 +1,14 @@
+[
+	{
+		"code": "constructor_state_reassignment",
+		"message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):3:2`",
+		"start": {
+			"line": 5,
+			"column": 2
+		},
+		"end": {
+			"line": 5,
+			"column": 24
+		}
+	}
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js
new file mode 100644
index 000000000000..e37be4b3e691
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+	constructor() {
+		this.count = $state(0);
+		this.count = 1;
+		this.count = $state(0);
+	}
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json
new file mode 100644
index 000000000000..8017794a679d
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json
@@ -0,0 +1,14 @@
+[
+	{
+		"code": "constructor_state_reassignment",
+		"message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):3:2`",
+		"start": {
+			"line": 5,
+			"column": 2
+		},
+		"end": {
+			"line": 5,
+			"column": 28
+		}
+	}
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js
new file mode 100644
index 000000000000..f9196ff3cd51
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+	constructor() {
+		this.count = $state(0);
+		this.count = 1;
+		this.count = $state.raw(0);
+	}
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json
new file mode 100644
index 000000000000..9f959874c80e
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json
@@ -0,0 +1,14 @@
+[
+	{
+		"code": "state_invalid_placement",
+		"message": "`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.",
+		"start": {
+			"line": 4,
+			"column": 16
+		},
+		"end": {
+			"line": 4,
+			"column": 25
+		}
+	}
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js
new file mode 100644
index 000000000000..bf1aada1b5df
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+	constructor() {
+		if (true) {
+			this.count = $state(0);
+		}
+	}
+}

From ac42ad5580125ab3eb99eeee7618d521a8100671 Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Tue, 29 Apr 2025 16:22:57 -0400
Subject: [PATCH 08/10] final cleanup??

---
 .../2-analyze/visitors/PropertyDefinition.js  |  2 +-
 .../visitors/shared/class-analysis.js         | 58 ++++++++++++++-----
 .../client/visitors/AssignmentExpression.js   |  2 +-
 .../visitors/shared/client-class-analysis.js  | 12 ++--
 .../server/visitors/AssignmentExpression.js   |  2 +-
 .../visitors/shared/server-class-analysis.js  | 11 ++--
 .../3-transform/shared/class_analysis.js      | 52 +++++++++++++----
 .../phases/3-transform/shared/types.d.ts      | 33 ++++++++---
 .../compiler/phases/3-transform/types.d.ts    |  2 +-
 packages/svelte/src/compiler/phases/nodes.js  |  1 +
 .../_config.js                                |  3 +
 .../main.svelte                               |  9 +++
 .../class-state-constructor-5/errors.json     | 14 +++++
 .../class-state-constructor-5/input.svelte.js |  7 +++
 .../class-state-constructor-6/errors.json     | 14 +++++
 .../class-state-constructor-6/input.svelte.js |  6 ++
 16 files changed, 182 insertions(+), 46 deletions(-)
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json
 create mode 100644 packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js

diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js
index 6bf5eb73d5db..aed7ff7fe8e8 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/PropertyDefinition.js
@@ -7,6 +7,6 @@
  * @param {Context} context
  */
 export function PropertyDefinition(node, context) {
-	context.state.class_state?.register?.(node, context);
+	context.state.class_state?.register(node, context);
 	context.next();
 }
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
index 4b3d758200b6..02b585fd4dba 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/class-analysis.js
@@ -1,4 +1,4 @@
-/** @import { AssignmentExpression, PropertyDefinition, Expression } from 'estree' */
+/** @import { AssignmentExpression, PropertyDefinition, Expression, Identifier, PrivateIdentifier, Literal, MethodDefinition } from 'estree' */
 /** @import { AST } from '#compiler' */
 /** @import { Context } from '../../types' */
 /** @import { StateCreationRuneName } from '../../../../../utils.js' */
@@ -15,9 +15,11 @@ import { is_state_creation_rune } from '../../../../../utils.js';
 const reassignable_assignments = new Set(['$state', '$state.raw', 'regular']);
 
 export class ClassAnalysis {
-	// TODO: Probably need to include property definitions here too
 	/** @type {Map<string, PropertyAssignmentDetails>} */
-	property_assignments = new Map();
+	#public_assignments = new Map();
+
+	/** @type {Map<string, PropertyAssignmentDetails>} */
+	#private_assignments = new Map();
 
 	/**
 	 * Determines if the node is a valid assignment to a class property, and if so,
@@ -30,30 +32,46 @@ export class ClassAnalysis {
 		let name;
 		/** @type {PropertyAssignmentType} */
 		let type;
+		/** @type {boolean} */
+		let is_private;
 
 		if (node.type === 'AssignmentExpression') {
 			if (!this.is_class_property_assignment_at_constructor_root(node, context.path)) {
 				return;
 			}
-			name = node.left.property.name;
+
+			let maybe_name = get_name_for_identifier(node.left.property);
+			if (!maybe_name) {
+				return;
+			}
+
+			name = maybe_name;
 			type = this.#get_assignment_type(node, context);
+			is_private = node.left.property.type === 'PrivateIdentifier';
 
-			this.#check_for_conflicts(node, name, type);
+			this.#check_for_conflicts(node, name, type, is_private);
 		} else {
 			if (!this.#is_assigned_property(node)) {
 				return;
 			}
 
-			name = node.key.name;
+			let maybe_name = get_name_for_identifier(node.key);
+			if (!maybe_name) {
+				return;
+			}
+
+			name = maybe_name;
 			type = this.#get_assignment_type(node, context);
+			is_private = node.key.type === 'PrivateIdentifier';
 
 			// we don't need to check for conflicts here because they're not possible yet
 		}
 
 		// we don't have to validate anything other than conflicts here, because the rune placement rules
 		// catch all of the other weirdness.
-		if (!this.property_assignments.has(name)) {
-			this.property_assignments.set(name, { type, node });
+		const map = is_private ? this.#private_assignments : this.#public_assignments;
+		if (!map.has(name)) {
+			map.set(name, { type, node });
 		}
 	}
 
@@ -61,7 +79,7 @@ export class ClassAnalysis {
 	 * @template {AST.SvelteNode} T
 	 * @param {AST.SvelteNode} node
 	 * @param {T[]} path
-	 * @returns {node is AssignmentExpression & { left: { type: 'MemberExpression' } & { object: { type: 'ThisExpression' }; property: { type: 'Identifier' | 'PrivateIdentifier' } } }}
+	 * @returns {node is AssignmentExpression & { left: { type: 'MemberExpression' } & { object: { type: 'ThisExpression' }; property: { type: 'Identifier' | 'PrivateIdentifier' | 'Literal' } } }}
 	 */
 	is_class_property_assignment_at_constructor_root(node, path) {
 		if (
@@ -71,7 +89,8 @@ export class ClassAnalysis {
 				node.left.type === 'MemberExpression' &&
 				node.left.object.type === 'ThisExpression' &&
 				(node.left.property.type === 'Identifier' ||
-					node.left.property.type === 'PrivateIdentifier')
+					node.left.property.type === 'PrivateIdentifier' ||
+					node.left.property.type === 'Literal')
 			)
 		) {
 			return false;
@@ -89,11 +108,13 @@ export class ClassAnalysis {
 	 * We only care about properties that have values assigned to them -- if they don't,
 	 * they can't be a conflict for state declared in the constructor.
 	 * @param {PropertyDefinition} node
-	 * @returns {node is PropertyDefinition & { key: { type: 'PrivateIdentifier' | 'Identifier' }; value: Expression; static: false; computed: false }}
+	 * @returns {node is PropertyDefinition & { key: { type: 'PrivateIdentifier' | 'Identifier' | 'Literal' }; value: Expression; static: false; computed: false }}
 	 */
 	#is_assigned_property(node) {
 		return (
-			(node.key.type === 'PrivateIdentifier' || node.key.type === 'Identifier') &&
+			(node.key.type === 'PrivateIdentifier' ||
+				node.key.type === 'Identifier' ||
+				node.key.type === 'Literal') &&
 			Boolean(node.value) &&
 			!node.static &&
 			!node.computed
@@ -107,9 +128,10 @@ export class ClassAnalysis {
 	 * @param {AssignmentExpression} node
 	 * @param {string} name
 	 * @param {PropertyAssignmentType} type
+	 * @param {boolean} is_private
 	 */
-	#check_for_conflicts(node, name, type) {
-		const existing = this.property_assignments.get(name);
+	#check_for_conflicts(node, name, type, is_private) {
+		const existing = (is_private ? this.#private_assignments : this.#public_assignments).get(name);
 		if (!existing) {
 			return;
 		}
@@ -140,3 +162,11 @@ export class ClassAnalysis {
 		return 'regular';
 	}
 }
+
+/**
+ *
+ * @param {PrivateIdentifier | Identifier | Literal} node
+ */
+function get_name_for_identifier(node) {
+	return node.type === 'Literal' ? node.value?.toString() : node.name;
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
index ff8d2b5ce12c..34d80d388ca1 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
@@ -17,7 +17,7 @@ import { validate_mutation } from './shared/utils.js';
  * @param {Context} context
  */
 export function AssignmentExpression(node, context) {
-	const stripped_node = context.state.class_analysis?.register_assignment(node, context);
+	const stripped_node = context.state.class_analysis?.generate_assignment(node, context);
 	if (stripped_node) {
 		return stripped_node;
 	}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js
index 70dd3646765b..778fea32e5c2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/client-class-analysis.js
@@ -56,7 +56,10 @@ export function create_client_class_analysis(body) {
 			left: {
 				...node.left,
 				// ...swap out the assignment to go directly against the private field
-				property: field.id
+				property: field.id,
+				// this could be a transformation from `this.[1]` to `this.#_` (the private field we generated)
+				// -- private fields are never computed
+				computed: false
 			},
 			// ...and swap out the assignment's value for the state field init
 			right: build_init_value(field.kind, node.right.arguments[0], context)
@@ -67,15 +70,14 @@ export function create_client_class_analysis(body) {
 }
 
 /**
- *
  * @param {StateCreationRuneName} kind
  * @param {Expression | SpreadElement} arg
  * @param {Context} context
  */
 function build_init_value(kind, arg, context) {
-	const init = /** @type {Expression} **/ (
-		context.visit(arg, { ...context.state, in_constructor: false })
-	);
+	const init = arg
+		? /** @type {Expression} **/ (context.visit(arg, { ...context.state, in_constructor: false }))
+		: b.void0;
 
 	switch (kind) {
 		case '$state':
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
index 9977c979db3f..ae84f0878228 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js
@@ -10,7 +10,7 @@ import { visit_assignment_expression } from '../../shared/assignments.js';
  * @param {Context} context
  */
 export function AssignmentExpression(node, context) {
-	const stripped_node = context.state.class_analysis?.register_assignment(node, context);
+	const stripped_node = context.state.class_analysis?.generate_assignment(node, context);
 	if (stripped_node) {
 		return stripped_node;
 	}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js
index 44a641508766..73390d0e38ad 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/server-class-analysis.js
@@ -23,11 +23,9 @@ export function create_server_class_analysis(body) {
 			// value can be assigned in the constructor.
 			value = null;
 		} else if (field.kind !== '$derived' && field.kind !== '$derived.by') {
-			return [/** @type {PropertyDefinition} */ (context.visit(node, context.state))];
+			return [/** @type {PropertyDefinition} */ (context.visit(node))];
 		} else {
-			const init = /** @type {Expression} **/ (
-				context.visit(node.value.arguments[0], context.state)
-			);
+			const init = /** @type {Expression} **/ (context.visit(node.value.arguments[0]));
 			value =
 				field.kind === '$derived.by' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
 		}
@@ -73,7 +71,8 @@ export function create_server_class_analysis(body) {
 			left: {
 				...node.left,
 				// ...swap out the assignment to go directly against the private field
-				property: field.id
+				property: field.id,
+				computed: false
 			},
 			// ...and swap out the assignment's value for the state field init
 			right: build_init_value(field.kind, node.right.arguments[0], context)
@@ -90,7 +89,7 @@ export function create_server_class_analysis(body) {
  * @param {Context} context
  */
 function build_init_value(kind, arg, context) {
-	const init = /** @type {Expression} **/ (context.visit(arg, context.state));
+	const init = arg ? /** @type {Expression} **/ (context.visit(arg)) : b.void0;
 
 	switch (kind) {
 		case '$state':
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js b/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js
index e482f10d49dc..20d9e75eca18 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/class_analysis.js
@@ -19,13 +19,25 @@ import { get_rune } from '../../scope.js';
  * @returns {ClassAnalysis<TContext>}
  */
 export function create_class_analysis(body, build_state_field, build_assignment) {
-	/** @type {Map<string, StateField>} */
+	/**
+	 * Public, stateful fields.
+	 * @type {Map<string, StateField>}
+	 */
 	const public_fields = new Map();
 
-	/** @type {Map<string, StateField>} */
+	/**
+	 * Private, stateful fields. These are namespaced separately because
+	 * public and private fields can have the same name in the AST -- ex.
+	 * `count` and `#count` are both named `count` -- and because it's useful
+	 * in a couple of cases to be able to check for only one or the other.
+	 * @type {Map<string, StateField>}
+	 */
 	const private_fields = new Map();
 
-	/** @type {Array<PropertyDefinition | MethodDefinition>} */
+	/**
+	 * Accumulates nodes for the new class body.
+	 * @type {Array<PropertyDefinition | MethodDefinition>}
+	 */
 	const new_body = [];
 
 	/**
@@ -57,6 +69,8 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 	const replacers = [];
 
 	/**
+	 * Get a state field by name.
+	 *
 	 * @param {string} name
 	 * @param {boolean} is_private
 	 * @param {ReadonlyArray<StateCreationRuneName>} [kinds]
@@ -69,20 +83,30 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 	}
 
 	/**
+	 * Create a child context that makes sense for passing to the child analyzers.
 	 * @param {TContext} context
 	 * @returns {TContext}
 	 */
 	function create_child_context(context) {
+		const state = {
+			...context.state,
+			class_analysis
+		};
+		// @ts-expect-error - I can't find a way to make TypeScript happy with these
+		const visit = (node, state_override) => context.visit(node, { ...state, ...state_override });
+		// @ts-expect-error - I can't find a way to make TypeScript happy with these
+		const next = (state_override) => context.next({ ...state, ...state_override });
 		return {
 			...context,
-			state: {
-				...context.state,
-				class_analysis
-			}
+			state,
+			visit,
+			next
 		};
 	}
 
 	/**
+	 * Generate a new body for the class. Ensure there is a visitor for AssignmentExpression that
+	 * calls `generate_assignment` to capture any stateful fields declared in the constructor.
 	 * @param {TContext} context
 	 */
 	function generate_body(context) {
@@ -107,11 +131,16 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 	}
 
 	/**
+	 * Given an assignment expression, check to see if that assignment expression declares
+	 * a stateful field. If it does, register that field and then return the processed
+	 * assignment expression. If an assignment expression is returned from this function,
+	 * it should be considered _fully processed_ and should replace the existing assignment
+	 * expression node.
 	 * @param {AssignmentExpression} node
 	 * @param {TContext} context
 	 * @returns {AssignmentExpression | null} The node, if `register_assignment` handled its transformation.
 	 */
-	function register_assignment(node, context) {
+	function generate_assignment(node, context) {
 		const child_context = create_child_context(context);
 		if (
 			!(
@@ -119,7 +148,8 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 				node.left.type === 'MemberExpression' &&
 				node.left.object.type === 'ThisExpression' &&
 				(node.left.property.type === 'Identifier' ||
-					node.left.property.type === 'PrivateIdentifier')
+					node.left.property.type === 'PrivateIdentifier' ||
+					node.left.property.type === 'Literal')
 			)
 		) {
 			return null;
@@ -177,6 +207,8 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 	}
 
 	/**
+	 * Register a class body definition.
+	 *
 	 * @param {PropertyDefinition | MethodDefinition | StaticBlock} node
 	 * @param {TContext} child_context
 	 * @returns {boolean} if this node is stateful and was registered
@@ -325,7 +357,7 @@ export function create_class_analysis(body, build_state_field, build_assignment)
 	const class_analysis = {
 		get_field,
 		generate_body,
-		register_assignment
+		generate_assignment
 	};
 
 	return class_analysis;
diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts
index 96fe27ca5dcc..2d02e705c317 100644
--- a/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/shared/types.d.ts
@@ -1,4 +1,14 @@
-import type { AssignmentExpression, CallExpression, Identifier, MemberExpression, PropertyDefinition, MethodDefinition, PrivateIdentifier, ThisExpression } from 'estree';
+import type {
+	AssignmentExpression,
+	CallExpression,
+	Identifier,
+	MemberExpression,
+	PropertyDefinition,
+	MethodDefinition,
+	PrivateIdentifier,
+	ThisExpression,
+	Literal
+} from 'estree';
 import type { StateField } from '../types';
 import type { Context as ServerContext } from '../server/types';
 import type { Context as ClientContext } from '../client/types';
@@ -7,13 +17,13 @@ import type { StateCreationRuneName } from '../../../../utils';
 export type StatefulAssignment = AssignmentExpression & {
 	left: MemberExpression & {
 		object: ThisExpression;
-		property: Identifier | PrivateIdentifier
+		property: Identifier | PrivateIdentifier | Literal;
 	};
 	right: CallExpression;
 };
 
 export type StatefulPropertyDefinition = PropertyDefinition & {
-	key: Identifier | PrivateIdentifier;
+	key: Identifier | PrivateIdentifier | Literal;
 	value: CallExpression;
 };
 
@@ -34,7 +44,9 @@ export type AssignmentBuilderParams<TContext extends ServerContext | ClientConte
 	context: TContext;
 };
 
-export type AssignmentBuilder<TContext  extends ServerContext | ClientContext> = (params: AssignmentBuilderParams<TContext>) => AssignmentExpression;
+export type AssignmentBuilder<TContext extends ServerContext | ClientContext> = (
+	params: AssignmentBuilderParams<TContext>
+) => AssignmentExpression;
 
 export type ClassAnalysis<TContext extends ServerContext | ClientContext> = {
 	/**
@@ -43,7 +55,11 @@ export type ClassAnalysis<TContext extends ServerContext | ClientContext> = {
 	 * @param kinds - What kinds of state creation runes you're looking for, eg. only '$derived.by'.
 	 * @returns The field if it exists and matches the given criteria, or null.
 	 */
-	get_field: (name: string, is_private: boolean, kinds?: Array<StateCreationRuneName>) => StateField | undefined;
+	get_field: (
+		name: string,
+		is_private: boolean,
+		kinds?: Array<StateCreationRuneName>
+	) => StateField | undefined;
 
 	/**
 	 * Given the body of a class, generate a new body with stateful fields.
@@ -59,5 +75,8 @@ export type ClassAnalysis<TContext extends ServerContext | ClientContext> = {
 	 * a state field on the class. If it is, it registers that state field and modifies the
 	 * assignment expression.
 	 */
-	register_assignment: (node: AssignmentExpression, context: TContext) => AssignmentExpression | null;
-}
+	generate_assignment: (
+		node: AssignmentExpression,
+		context: TContext
+	) => AssignmentExpression | null;
+};
diff --git a/packages/svelte/src/compiler/phases/3-transform/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/types.d.ts
index 8999f096142e..c610110cef9d 100644
--- a/packages/svelte/src/compiler/phases/3-transform/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/types.d.ts
@@ -14,4 +14,4 @@ export interface TransformState {
 export interface StateField {
 	kind: StateCreationRuneName;
 	id: PrivateIdentifier;
-}
\ No newline at end of file
+}
diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js
index 003c3a2c4945..29fb8c5c83c2 100644
--- a/packages/svelte/src/compiler/phases/nodes.js
+++ b/packages/svelte/src/compiler/phases/nodes.js
@@ -1,4 +1,5 @@
 /** @import { AST, ExpressionMetadata } from '#compiler' */
+
 /**
  * All nodes that can appear elsewhere than the top level, have attributes and can contain children
  */
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js
new file mode 100644
index 000000000000..f47bee71df87
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js
@@ -0,0 +1,3 @@
+import { test } from '../../test';
+
+export default test({});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte
new file mode 100644
index 000000000000..e2c4f302b397
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte
@@ -0,0 +1,9 @@
+<script>
+	class Test {
+		0 = $state();
+
+		constructor() {
+			this[1] = $state();
+		}
+	}
+</script>
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json
new file mode 100644
index 000000000000..359274d15661
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json
@@ -0,0 +1,14 @@
+[
+	{
+		"code": "constructor_state_reassignment",
+		"message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):2:1`",
+		"start": {
+			"line": 4,
+			"column": 2
+		},
+		"end": {
+			"line": 4,
+			"column": 27
+		}
+	}
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js
new file mode 100644
index 000000000000..bc3d19a14fae
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js
@@ -0,0 +1,7 @@
+export class Counter {
+	// prettier-ignore
+	'count' = $state(0);
+	constructor() {
+		this['count'] = $state(0);
+	}
+}
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json
new file mode 100644
index 000000000000..359274d15661
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json
@@ -0,0 +1,14 @@
+[
+	{
+		"code": "constructor_state_reassignment",
+		"message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):2:1`",
+		"start": {
+			"line": 4,
+			"column": 2
+		},
+		"end": {
+			"line": 4,
+			"column": 27
+		}
+	}
+]
diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js
new file mode 100644
index 000000000000..2ebe52e685ed
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js
@@ -0,0 +1,6 @@
+export class Counter {
+	count = $state(0);
+	constructor() {
+		this['count'] = $state(0);
+	}
+}

From b44eed9a091c360a31d3b0529c7fbd555dc1fffe Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Tue, 29 Apr 2025 16:49:00 -0400
Subject: [PATCH 09/10] tests

---
 .../validator/samples/class-state-constructor-5/errors.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json
index 359274d15661..7942b016cb2b 100644
--- a/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json
+++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json
@@ -1,13 +1,13 @@
 [
 	{
 		"code": "constructor_state_reassignment",
-		"message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):2:1`",
+		"message": "Cannot redeclare stateful field `count` in the constructor. The field was originally declared here: `(unknown):3:1`",
 		"start": {
-			"line": 4,
+			"line": 5,
 			"column": 2
 		},
 		"end": {
-			"line": 4,
+			"line": 5,
 			"column": 27
 		}
 	}

From 12a02b7eed5c3b01bc4be6ea2f060ecd3a8d5e4d Mon Sep 17 00:00:00 2001
From: "S. Elliott Johnson" <sejohnson@torchcloudconsulting.com>
Date: Tue, 29 Apr 2025 17:00:13 -0400
Subject: [PATCH 10/10] test for better types

---
 .../_config.js                                | 20 +++++++++++++++++++
 .../main.svelte                               | 12 +++++++++++
 2 files changed, 32 insertions(+)
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js
 create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte

diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js
new file mode 100644
index 000000000000..02cf36d900cc
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js
@@ -0,0 +1,20 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	html: `<button>0</button>`,
+
+	test({ assert, target }) {
+		const btn = target.querySelector('button');
+
+		flushSync(() => {
+			btn?.click();
+		});
+		assert.htmlEqual(target.innerHTML, `<button>1</button>`);
+
+		flushSync(() => {
+			btn?.click();
+		});
+		assert.htmlEqual(target.innerHTML, `<button>2</button>`);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte
new file mode 100644
index 000000000000..5dbbb10afd35
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte
@@ -0,0 +1,12 @@
+<script>
+	class Counter {
+		count;
+
+		constructor(count) {
+			this.count = $state(count);
+		}
+	}
+	const counter = new Counter(0);
+</script>
+
+<button onclick={() => counter.count++}>{counter.count}</button>