;
}) {
const parent_inline_component = node.parent as InlineComponent;
@@ -31,7 +31,8 @@ export default function(node: SlotTemplate | Element | InlineComponent, renderer
options.slot_scopes.set(node.slot_template_name, {
input: get_slot_scope(node.lets),
- output: slot_fragment_content
+ output: slot_fragment_content,
+ statements: get_const_tags(node.const_tags)
});
}
}
diff --git a/src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts b/src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts
new file mode 100644
index 0000000000..2c156609cd
--- /dev/null
+++ b/src/compiler/compile/render_ssr/handlers/shared/get_const_tags.ts
@@ -0,0 +1,17 @@
+import ConstTag from '../../../nodes/ConstTag';
+
+export function get_const_tags(const_tags: ConstTag[]) {
+ if (const_tags.length === 0) return null;
+ return {
+ type: 'VariableDeclaration',
+ kind: 'let',
+ declarations: const_tags.map(const_tag => {
+ const assignment = const_tag.node.expression;
+ return {
+ type: 'VariableDeclarator',
+ id: assignment.left,
+ init: assignment.right
+ };
+ })
+ };
+}
diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts
index 3e54ea55a3..63dc29eec6 100644
--- a/src/compiler/interfaces.ts
+++ b/src/compiler/interfaces.ts
@@ -1,4 +1,4 @@
-import { Node, Program } from 'estree';
+import { AssignmentExpression, Node, Program } from 'estree';
import { SourceMap } from 'magic-string';
interface BaseNode {
@@ -20,7 +20,7 @@ export interface Text extends BaseNode {
}
export interface MustacheTag extends BaseNode {
- type: 'MustacheTag';
+ type: 'MustacheTag' | 'RawMustacheTag';
expression: Node;
}
@@ -30,6 +30,16 @@ export interface Comment extends BaseNode {
ignores: string[];
}
+export interface ConstTag extends BaseNode {
+ type: 'ConstTag';
+ expression: AssignmentExpression;
+}
+
+interface DebugTag extends BaseNode {
+ type: 'DebugTag';
+ identifiers: Node[]
+}
+
export type DirectiveType = 'Action'
| 'Animation'
| 'Binding'
@@ -73,6 +83,8 @@ export interface Transition extends BaseDirective {
export type Directive = BaseDirective | Transition;
export type TemplateNode = Text
+| ConstTag
+| DebugTag
| MustacheTag
| BaseNode
| Element
diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts
index 584f9d6e9a..da9016a9df 100644
--- a/src/compiler/parse/state/mustache.ts
+++ b/src/compiler/parse/state/mustache.ts
@@ -375,6 +375,28 @@ export default function mustache(parser: Parser) {
type: 'DebugTag',
identifiers
});
+ } else if (parser.eat('@const')) {
+ // {@const a = b}
+ parser.require_whitespace();
+
+ const expression = read_expression(parser);
+
+ if (!(expression.type === 'AssignmentExpression' && expression.operator === '=')) {
+ parser.error({
+ code: 'invalid-const-args',
+ message: '{@const ...} must be an assignment.'
+ }, start);
+ }
+
+ parser.allow_whitespace();
+ parser.eat('}', true);
+
+ parser.current().children.push({
+ start,
+ end: parser.index,
+ type: 'ConstTag',
+ expression
+ });
} else {
const expression = read_expression(parser);
diff --git a/src/compiler/parse/utils/node.ts b/src/compiler/parse/utils/node.ts
index 0d39529b59..944bdb4c58 100644
--- a/src/compiler/parse/utils/node.ts
+++ b/src/compiler/parse/utils/node.ts
@@ -19,6 +19,8 @@ export function to_string(node: TemplateNode) {
return '{@html} block';
case 'DebugTag':
return '{@debug} block';
+ case 'ConstTag':
+ return '{@const} tag';
case 'Element':
case 'InlineComponent':
case 'Slot':
diff --git a/test/js/samples/debug-ssr-foo/expected.js b/test/js/samples/debug-ssr-foo/expected.js
index 69da37b2d9..e0a29f18de 100644
--- a/test/js/samples/debug-ssr-foo/expected.js
+++ b/test/js/samples/debug-ssr-foo/expected.js
@@ -7,8 +7,10 @@ const Component = create_ssr_component(($$result, $$props, $$bindings, slots) =>
if ($$props.things === void 0 && $$bindings.things && things !== void 0) $$bindings.things(things);
if ($$props.foo === void 0 && $$bindings.foo && foo !== void 0) $$bindings.foo(foo);
- return `${each(things, thing => `${escape(thing.name)}
- ${debug(null, 7, 2, { foo })}`)}
+ return `${each(things, thing => {
+ return `${escape(thing.name)}
+ ${debug(null, 7, 2, { foo })}`;
+ })}
foo: ${escape(foo)}
`;
});
diff --git a/test/runtime/samples/const-tag-await-then-destructuring/_config.js b/test/runtime/samples/const-tag-await-then-destructuring/_config.js
new file mode 100644
index 0000000000..cb40e7c456
--- /dev/null
+++ b/test/runtime/samples/const-tag-await-then-destructuring/_config.js
@@ -0,0 +1,19 @@
+export default {
+ html: '12 120 70, 30+4=34
',
+ async test({ component, target, assert }) {
+ component.promise1 = Promise.resolve({width: 5, height: 6});
+ component.promise2 = Promise.reject({width: 6, height: 7});
+
+ await Promise.resolve();
+ assert.htmlEqual(target.innerHTML, `
+ 30 300 110, 50+6=56
+ 42 420 130, 60+7=67
+ `);
+
+ component.constant = 20;
+ assert.htmlEqual(target.innerHTML, `
+ 30 600 220, 100+6=106
+ 42 840 260, 120+7=127
+ `);
+ }
+};
diff --git a/test/runtime/samples/const-tag-await-then-destructuring/main.svelte b/test/runtime/samples/const-tag-await-then-destructuring/main.svelte
new file mode 100644
index 0000000000..ab450822c6
--- /dev/null
+++ b/test/runtime/samples/const-tag-await-then-destructuring/main.svelte
@@ -0,0 +1,23 @@
+
+
+{#await promise1 then { width, height }}
+ {@const {area, volume} = calculate(width, height, constant)}
+ {@const perimeter = (width + height) * constant}
+ {@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
+ {area} {volume} {perimeter}, {_width}+{_height}={sum}
+{/await}
+
+{#await promise2 catch { width, height }}
+ {@const {area, volume} = calculate(width, height, constant)}
+ {@const perimeter = (width + height) * constant}
+ {@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
+ {area} {volume} {perimeter}, {_width}+{_height}={sum}
+{/await}
\ No newline at end of file
diff --git a/test/runtime/samples/const-tag-await-then/_config.js b/test/runtime/samples/const-tag-await-then/_config.js
new file mode 100644
index 0000000000..cb40e7c456
--- /dev/null
+++ b/test/runtime/samples/const-tag-await-then/_config.js
@@ -0,0 +1,19 @@
+export default {
+ html: '12 120 70, 30+4=34
',
+ async test({ component, target, assert }) {
+ component.promise1 = Promise.resolve({width: 5, height: 6});
+ component.promise2 = Promise.reject({width: 6, height: 7});
+
+ await Promise.resolve();
+ assert.htmlEqual(target.innerHTML, `
+ 30 300 110, 50+6=56
+ 42 420 130, 60+7=67
+ `);
+
+ component.constant = 20;
+ assert.htmlEqual(target.innerHTML, `
+ 30 600 220, 100+6=106
+ 42 840 260, 120+7=127
+ `);
+ }
+};
diff --git a/test/runtime/samples/const-tag-await-then/main.svelte b/test/runtime/samples/const-tag-await-then/main.svelte
new file mode 100644
index 0000000000..f8eed87115
--- /dev/null
+++ b/test/runtime/samples/const-tag-await-then/main.svelte
@@ -0,0 +1,23 @@
+
+
+{#await promise1 then box}
+ {@const {area, volume} = calculate(box.width, box.height, constant)}
+ {@const perimeter = (box.width + box.height) * constant}
+ {@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
+ {area} {volume} {perimeter}, {width}+{height}={sum}
+{/await}
+
+{#await promise2 catch box}
+ {@const {area, volume} = calculate(box.width, box.height, constant)}
+ {@const perimeter = (box.width + box.height) * constant}
+ {@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
+ {area} {volume} {perimeter}, {width}+{height}={sum}
+{/await}
\ No newline at end of file
diff --git a/test/runtime/samples/const-tag-component/Component.svelte b/test/runtime/samples/const-tag-component/Component.svelte
new file mode 100644
index 0000000000..68ba82f245
--- /dev/null
+++ b/test/runtime/samples/const-tag-component/Component.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/test/runtime/samples/const-tag-component/_config.js b/test/runtime/samples/const-tag-component/_config.js
new file mode 100644
index 0000000000..66399a06fa
--- /dev/null
+++ b/test/runtime/samples/const-tag-component/_config.js
@@ -0,0 +1,46 @@
+export default {
+ html: `
+ 12 120 70, 30+4=34
+ 12 120 70, 30+4=34
+ 12 120 70, 30+4=34
+
+
+ 12 120 70, 30+4=34
+ 12 120 70, 30+4=34
+ `,
+ async test({ component, target, assert }) {
+ component.constant = 20;
+ assert.htmlEqual(target.innerHTML, `
+ 12 240 140, 60+4=64
+ 12 240 140, 60+4=64
+ 12 240 140, 60+4=64
+
+
+ 12 240 140, 60+4=64
+ 12 240 140, 60+4=64
+ `);
+
+ component.box = {width: 5, height: 6};
+ assert.htmlEqual(target.innerHTML, `
+ 30 600 220, 100+6=106
+ 30 600 220, 100+6=106
+ 30 600 220, 100+6=106
+
+
30 600 220, 100+6=106
+
+
+
30 600 220, 100+6=106
+
+ 30 600 220, 100+6=106
+ 30 600 220, 100+6=106
+ `);
+ }
+};
diff --git a/test/runtime/samples/const-tag-component/main.svelte b/test/runtime/samples/const-tag-component/main.svelte
new file mode 100644
index 0000000000..1854c3fba9
--- /dev/null
+++ b/test/runtime/samples/const-tag-component/main.svelte
@@ -0,0 +1,60 @@
+
+
+
+
+ {@const {area, volume} = calculate(box.width, box.height, constant)}
+ {@const perimeter = (box.width + box.height) * constant}
+ {@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
+ {area} {volume} {perimeter}, {width}+{height}={sum}
+
+
+
+ {@const {area, volume} = calculate(width, height, constant)}
+ {@const perimeter = (width + height) * constant}
+ {@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
+ {area} {volume} {perimeter}, {_width}+{_height}={sum}
+
+
+
+ {@const {area, volume} = calculate(width, height, constant)}
+ {@const perimeter = (width + height) * constant}
+ {@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
+ {area} {volume} {perimeter}, {_width}+{_height}={sum}
+
+
+
+
+
+ {@const {area, volume} = calculate(box.width, box.height, constant)}
+ {@const perimeter = (box.width + box.height) * constant}
+ {@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
+
{area} {volume} {perimeter}, {width}+{height}={sum}
+
+
+
+ {@const {area, volume} = calculate(width, height, constant)}
+ {@const perimeter = (width + height) * constant}
+ {@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
+
{area} {volume} {perimeter}, {_width}+{_height}={sum}
+
+
+ {@const {area, volume} = calculate(box.width, box.height, constant)}
+ {@const perimeter = (box.width + box.height) * constant}
+ {@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
+ {area} {volume} {perimeter}, {width}+{height}={sum}
+
+
+
+ {@const {area, volume} = calculate(width, height, constant)}
+ {@const perimeter = (width + height) * constant}
+ {@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
+ {area} {volume} {perimeter}, {_width}+{_height}={sum}
+
diff --git a/test/runtime/samples/const-tag-dependencies/_config.js b/test/runtime/samples/const-tag-dependencies/_config.js
new file mode 100644
index 0000000000..5cbb2da587
--- /dev/null
+++ b/test/runtime/samples/const-tag-dependencies/_config.js
@@ -0,0 +1,12 @@
+export default {
+ html: `
+ 7
+ `,
+ async test({ component, target, assert }) {
+ component.a = 5;
+
+ assert.htmlEqual(target.innerHTML, `
+ 9
+ `);
+ }
+};
diff --git a/test/runtime/samples/const-tag-dependencies/main.svelte b/test/runtime/samples/const-tag-dependencies/main.svelte
new file mode 100644
index 0000000000..cce3b0c1f7
--- /dev/null
+++ b/test/runtime/samples/const-tag-dependencies/main.svelte
@@ -0,0 +1,10 @@
+
+
+{#each [value] as n}
+ {@const ab = a + b}
+ {ab}
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/const-tag-each-destructure/_config.js b/test/runtime/samples/const-tag-each-destructure/_config.js
new file mode 100644
index 0000000000..00f8b31540
--- /dev/null
+++ b/test/runtime/samples/const-tag-each-destructure/_config.js
@@ -0,0 +1,30 @@
+export default {
+ html: `
+ 12 120 70, 30+4=34
+ 35 350 120, 50+7=57
+ 48 480 140, 60+8=68
+ `,
+ async test({ component, target, assert }) {
+ component.constant = 20;
+
+ assert.htmlEqual(target.innerHTML, `
+ 12 240 140, 60+4=64
+ 35 700 240, 100+7=107
+ 48 960 280, 120+8=128
+ `);
+
+ component.boxes = [
+ {width: 3, height: 4},
+ {width: 4, height: 5},
+ {width: 5, height: 6},
+ {width: 6, height: 7}
+ ];
+
+ assert.htmlEqual(target.innerHTML, `
+ 12 240 140, 60+4=64
+ 20 400 180, 80+5=85
+ 30 600 220, 100+6=106
+ 42 840 260, 120+7=127
+ `);
+ }
+};
diff --git a/test/runtime/samples/const-tag-each-destructure/main.svelte b/test/runtime/samples/const-tag-each-destructure/main.svelte
new file mode 100644
index 0000000000..7f0391d5e6
--- /dev/null
+++ b/test/runtime/samples/const-tag-each-destructure/main.svelte
@@ -0,0 +1,19 @@
+
+
+{#each boxes as { width, height }}
+ {@const {area, volume} = calculate(width, height, constant)}
+ {@const perimeter = (width + height) * constant}
+ {@const [_width, _height, sum] = [width * constant, height, width * constant + height]}
+ {area} {volume} {perimeter}, {_width}+{_height}={sum}
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/const-tag-each/_config.js b/test/runtime/samples/const-tag-each/_config.js
new file mode 100644
index 0000000000..00f8b31540
--- /dev/null
+++ b/test/runtime/samples/const-tag-each/_config.js
@@ -0,0 +1,30 @@
+export default {
+ html: `
+ 12 120 70, 30+4=34
+ 35 350 120, 50+7=57
+ 48 480 140, 60+8=68
+ `,
+ async test({ component, target, assert }) {
+ component.constant = 20;
+
+ assert.htmlEqual(target.innerHTML, `
+ 12 240 140, 60+4=64
+ 35 700 240, 100+7=107
+ 48 960 280, 120+8=128
+ `);
+
+ component.boxes = [
+ {width: 3, height: 4},
+ {width: 4, height: 5},
+ {width: 5, height: 6},
+ {width: 6, height: 7}
+ ];
+
+ assert.htmlEqual(target.innerHTML, `
+ 12 240 140, 60+4=64
+ 20 400 180, 80+5=85
+ 30 600 220, 100+6=106
+ 42 840 260, 120+7=127
+ `);
+ }
+};
diff --git a/test/runtime/samples/const-tag-each/main.svelte b/test/runtime/samples/const-tag-each/main.svelte
new file mode 100644
index 0000000000..004f81cd83
--- /dev/null
+++ b/test/runtime/samples/const-tag-each/main.svelte
@@ -0,0 +1,19 @@
+
+
+{#each boxes as box}
+ {@const {area, volume} = calculate(box.width, box.height, constant)}
+ {@const perimeter = (box.width + box.height) * constant}
+ {@const [width, height, sum] = [box.width * constant, box.height, box.width * constant + box.height]}
+ {area} {volume} {perimeter}, {width}+{height}={sum}
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/const-tag-hoisting/_config.js b/test/runtime/samples/const-tag-hoisting/_config.js
new file mode 100644
index 0000000000..f8f4a5c85e
--- /dev/null
+++ b/test/runtime/samples/const-tag-hoisting/_config.js
@@ -0,0 +1,12 @@
+export default {
+ html: `
+ 4 ^ 4 = 256
+ `,
+ async test({ component, target, assert }) {
+ component.value = 3;
+
+ assert.htmlEqual(target.innerHTML, `
+ 3 ^ 4 = 81
+ `);
+ }
+};
diff --git a/test/runtime/samples/const-tag-hoisting/main.svelte b/test/runtime/samples/const-tag-hoisting/main.svelte
new file mode 100644
index 0000000000..49fce93274
--- /dev/null
+++ b/test/runtime/samples/const-tag-hoisting/main.svelte
@@ -0,0 +1,11 @@
+
+
+{#each [value] as n}
+ {n} ^ 4 = {hypercubed}
+
+ {@const squared = n * n}
+ {@const cubed = squared * n}
+ {@const hypercubed = cubed * n}
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/const-tag-ordering/_config.js b/test/runtime/samples/const-tag-ordering/_config.js
new file mode 100644
index 0000000000..f8f4a5c85e
--- /dev/null
+++ b/test/runtime/samples/const-tag-ordering/_config.js
@@ -0,0 +1,12 @@
+export default {
+ html: `
+ 4 ^ 4 = 256
+ `,
+ async test({ component, target, assert }) {
+ component.value = 3;
+
+ assert.htmlEqual(target.innerHTML, `
+ 3 ^ 4 = 81
+ `);
+ }
+};
diff --git a/test/runtime/samples/const-tag-ordering/main.svelte b/test/runtime/samples/const-tag-ordering/main.svelte
new file mode 100644
index 0000000000..49fce93274
--- /dev/null
+++ b/test/runtime/samples/const-tag-ordering/main.svelte
@@ -0,0 +1,11 @@
+
+
+{#each [value] as n}
+ {n} ^ 4 = {hypercubed}
+
+ {@const squared = n * n}
+ {@const cubed = squared * n}
+ {@const hypercubed = cubed * n}
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/const-tag-shadow/_config.js b/test/runtime/samples/const-tag-shadow/_config.js
new file mode 100644
index 0000000000..312235f113
--- /dev/null
+++ b/test/runtime/samples/const-tag-shadow/_config.js
@@ -0,0 +1,43 @@
+export default {
+ html: `
+ 7
+ 11
+ 15
+ 7
+ 19
+ 23
+ 27
+ 19
+ `,
+ async test({ component, target, assert }) {
+ component.numbers = [
+ {
+ a: 4,
+ b: 5,
+ children: [
+ { a: 6, b: 7 },
+ { a: 8, b: 9 }
+ ]
+ },
+ {
+ a: 10,
+ b: 11,
+ children: [
+ { a: 12, b: 13 },
+ { a: 14, b: 15 }
+ ]
+ }
+ ];
+
+ assert.htmlEqual(target.innerHTML, `
+ 9
+ 13
+ 17
+ 9
+ 21
+ 25
+ 29
+ 21
+ `);
+ }
+};
diff --git a/test/runtime/samples/const-tag-shadow/main.svelte b/test/runtime/samples/const-tag-shadow/main.svelte
new file mode 100644
index 0000000000..725121c733
--- /dev/null
+++ b/test/runtime/samples/const-tag-shadow/main.svelte
@@ -0,0 +1,31 @@
+
+
+{#each numbers as {a, b, children}}
+ {@const ab = a + b}
+ {ab}
+ {#each children as {a, b}}
+ {@const ab = a + b}
+ {ab}
+ {/each}
+ {ab}
+{/each}
\ No newline at end of file
diff --git a/test/validator/samples/const-tag-conflict-1/errors.json b/test/validator/samples/const-tag-conflict-1/errors.json
new file mode 100644
index 0000000000..59c72eccaf
--- /dev/null
+++ b/test/validator/samples/const-tag-conflict-1/errors.json
@@ -0,0 +1,9 @@
+[
+ {
+ "code": "invalid-const-declaration",
+ "message": "'a' has already been declared",
+ "start": { "line": 7, "column": 2, "character": 84 },
+ "end": { "line": 7, "column": 19, "character": 101 },
+ "pos": 84
+ }
+]
diff --git a/test/validator/samples/const-tag-conflict-1/input.svelte b/test/validator/samples/const-tag-conflict-1/input.svelte
new file mode 100644
index 0000000000..6633eeacdf
--- /dev/null
+++ b/test/validator/samples/const-tag-conflict-1/input.svelte
@@ -0,0 +1,8 @@
+
+
+{#each array as item}
+ {@const a = item}
+ {@const a = item}
+{/each}
\ No newline at end of file
diff --git a/test/validator/samples/const-tag-conflict-2/errors.json b/test/validator/samples/const-tag-conflict-2/errors.json
new file mode 100644
index 0000000000..11e2535091
--- /dev/null
+++ b/test/validator/samples/const-tag-conflict-2/errors.json
@@ -0,0 +1,9 @@
+[
+ {
+ "code": "invalid-const-declaration",
+ "message": "'item' has already been declared",
+ "start": { "line": 6, "column": 2, "character": 64 },
+ "end": { "line": 6, "column": 21, "character": 83 },
+ "pos": 64
+ }
+]
diff --git a/test/validator/samples/const-tag-conflict-2/input.svelte b/test/validator/samples/const-tag-conflict-2/input.svelte
new file mode 100644
index 0000000000..2be7207691
--- /dev/null
+++ b/test/validator/samples/const-tag-conflict-2/input.svelte
@@ -0,0 +1,7 @@
+
+
+{#each array as item}
+ {@const item = 123}
+{/each}
\ No newline at end of file
diff --git a/test/validator/samples/const-tag-cyclical/errors.json b/test/validator/samples/const-tag-cyclical/errors.json
new file mode 100644
index 0000000000..1436747798
--- /dev/null
+++ b/test/validator/samples/const-tag-cyclical/errors.json
@@ -0,0 +1,9 @@
+[
+ {
+ "code": "cyclical-const-tags",
+ "message": "Cyclical dependency detected: b → c → b",
+ "start": { "line": 6, "column": 2, "character": 61 },
+ "end": { "line": 6, "column": 20, "character": 79 },
+ "pos": 61
+ }
+]
diff --git a/test/validator/samples/const-tag-cyclical/input.svelte b/test/validator/samples/const-tag-cyclical/input.svelte
new file mode 100644
index 0000000000..2f694be808
--- /dev/null
+++ b/test/validator/samples/const-tag-cyclical/input.svelte
@@ -0,0 +1,8 @@
+
+
+{#each array as a}
+ {@const b = a + c}
+ {@const c = b + a}
+{/each}
\ No newline at end of file
diff --git a/test/validator/samples/const-tag-out-of-scope/input.svelte b/test/validator/samples/const-tag-out-of-scope/input.svelte
new file mode 100644
index 0000000000..d1c2723e22
--- /dev/null
+++ b/test/validator/samples/const-tag-out-of-scope/input.svelte
@@ -0,0 +1,10 @@
+
+
+{#each array as a}
+ {@const b = a + 1}
+
+{/each}
+
+{b}
\ No newline at end of file
diff --git a/test/validator/samples/const-tag-out-of-scope/warnings.json b/test/validator/samples/const-tag-out-of-scope/warnings.json
new file mode 100644
index 0000000000..c85b417fe4
--- /dev/null
+++ b/test/validator/samples/const-tag-out-of-scope/warnings.json
@@ -0,0 +1,17 @@
+[
+ {
+ "code": "missing-declaration",
+ "message": "'b' is not defined",
+ "pos": 100,
+ "start": {
+ "character": 100,
+ "column": 1,
+ "line": 10
+ },
+ "end": {
+ "character": 101,
+ "column": 2,
+ "line": 10
+ }
+ }
+]
diff --git a/test/validator/samples/const-tag-placement-1/errors.json b/test/validator/samples/const-tag-placement-1/errors.json
new file mode 100644
index 0000000000..52de84e2be
--- /dev/null
+++ b/test/validator/samples/const-tag-placement-1/errors.json
@@ -0,0 +1,9 @@
+[
+ {
+ "code": "invalid-const-placement",
+ "message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, or ",
+ "start": { "line": 5, "column": 0, "character": 36 },
+ "end": { "line": 5, "column": 18, "character": 54 },
+ "pos": 36
+ }
+]
diff --git a/test/validator/samples/const-tag-placement-1/input.svelte b/test/validator/samples/const-tag-placement-1/input.svelte
new file mode 100644
index 0000000000..14ea3085e7
--- /dev/null
+++ b/test/validator/samples/const-tag-placement-1/input.svelte
@@ -0,0 +1,5 @@
+
+
+{@const b = a + 1}
\ No newline at end of file
diff --git a/test/validator/samples/const-tag-placement-2/errors.json b/test/validator/samples/const-tag-placement-2/errors.json
new file mode 100644
index 0000000000..005b3ff8b7
--- /dev/null
+++ b/test/validator/samples/const-tag-placement-2/errors.json
@@ -0,0 +1,9 @@
+[
+ {
+ "code": "invalid-const-placement",
+ "message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, or ",
+ "start": { "line": 6, "column": 2, "character": 46 },
+ "end": { "line": 6, "column": 20, "character": 64 },
+ "pos": 46
+ }
+]
diff --git a/test/validator/samples/const-tag-placement-2/input.svelte b/test/validator/samples/const-tag-placement-2/input.svelte
new file mode 100644
index 0000000000..a843f9d58f
--- /dev/null
+++ b/test/validator/samples/const-tag-placement-2/input.svelte
@@ -0,0 +1,7 @@
+
+
+{#if a}
+ {@const b = a + 1}
+{/if}
\ No newline at end of file
diff --git a/test/validator/samples/const-tag-placement-3/errors.json b/test/validator/samples/const-tag-placement-3/errors.json
new file mode 100644
index 0000000000..cc0ba4d74d
--- /dev/null
+++ b/test/validator/samples/const-tag-placement-3/errors.json
@@ -0,0 +1,9 @@
+[
+ {
+ "code": "invalid-const-placement",
+ "message": "{@const} must be the immediate child of {#each}, {:then}, {:catch}, or ",
+ "start": { "line": 7, "column": 4, "character": 63 },
+ "end": { "line": 7, "column": 18, "character": 77 },
+ "pos": 63
+ }
+]
diff --git a/test/validator/samples/const-tag-placement-3/input.svelte b/test/validator/samples/const-tag-placement-3/input.svelte
new file mode 100644
index 0000000000..2519482a98
--- /dev/null
+++ b/test/validator/samples/const-tag-placement-3/input.svelte
@@ -0,0 +1,9 @@
+
+
+{#each a as i}
+
+ {@const b = i}
+
+{/each}
\ No newline at end of file
diff --git a/test/validator/samples/const-tag-readonly-1/errors.json b/test/validator/samples/const-tag-readonly-1/errors.json
new file mode 100644
index 0000000000..ed60c529fd
--- /dev/null
+++ b/test/validator/samples/const-tag-readonly-1/errors.json
@@ -0,0 +1,9 @@
+[
+ {
+ "code": "invalid-const-update",
+ "message": "'b' is declared using {@const ...} and is read-only",
+ "start": { "line": 7, "column": 26, "character": 106 },
+ "end": { "line": 7, "column": 30, "character": 110 },
+ "pos": 106
+ }
+]
diff --git a/test/validator/samples/const-tag-readonly-1/input.svelte b/test/validator/samples/const-tag-readonly-1/input.svelte
new file mode 100644
index 0000000000..b5a37c8608
--- /dev/null
+++ b/test/validator/samples/const-tag-readonly-1/input.svelte
@@ -0,0 +1,8 @@
+
+
+{#each array as a}
+ {@const b = a + 1}
+