diff --git a/src/basis/ListBasis.test.ts b/src/basis/ListBasis.test.ts
index f67a37c97..4a5e8f932 100644
--- a/src/basis/ListBasis.test.ts
+++ b/src/basis/ListBasis.test.ts
@@ -2,6 +2,8 @@ import { test, expect } from 'vitest';
import evaluateCode from '../runtime/evaluate';
test.each([
+ ['[1 2 3 :[4 5 6]]', '[1 2 3 4 5 6]'],
+ ['[:[1 2 3] :[4 5 6]]', '[1 2 3 4 5 6]'],
['[1 2 3].add(4)', '[1 2 3 4]'],
['[1 2 3].has(4)', '⊥'],
['[1 2 3].has(3)', '⊤'],
diff --git a/src/components/editor/SpreadView.svelte b/src/components/editor/SpreadView.svelte
new file mode 100644
index 000000000..0f1d17d1f
--- /dev/null
+++ b/src/components/editor/SpreadView.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/src/components/editor/util/nodeToView.ts b/src/components/editor/util/nodeToView.ts
index c10a82d87..073e5315a 100644
--- a/src/components/editor/util/nodeToView.ts
+++ b/src/components/editor/util/nodeToView.ts
@@ -1,3 +1,5 @@
+import type { ComponentType, SvelteComponent } from 'svelte';
+
/* eslint-disable @typescript-eslint/ban-types */
import BlockView from '../BlockView.svelte';
import BorrowView from '../BorrowView.svelte';
@@ -80,6 +82,7 @@ import TranslationView from '../TranslationView.svelte';
import FormattedLiteralView from '../FormattedLiteralView.svelte';
import FormattedTranslationView from '../FormattedTranslationView.svelte';
import IsLocaleView from '../IsLocaleView.svelte';
+import SpreadView from '../SpreadView.svelte';
import type Node from '@nodes/Node';
import Program from '@nodes/Program';
@@ -164,7 +167,7 @@ import Translation from '@nodes/Translation';
import FormattedTranslation from '@nodes/FormattedTranslation';
import FormattedLiteral from '@nodes/FormattedLiteral';
import IsLocale from '@nodes/IsLocale';
-import type { ComponentType, SvelteComponent } from 'svelte';
+import Spread from '@nodes/Spread';
const nodeToView = new Map>();
@@ -240,6 +243,7 @@ nodeToView.set(SetType, SetTypeView);
nodeToView.set(MapType, MapTypeView);
nodeToView.set(ListLiteral, ListLiteralView);
+nodeToView.set(Spread, SpreadView);
nodeToView.set(ListAccess, ListAccessView);
nodeToView.set(ListType, ListTypeView);
diff --git a/src/components/palette/editOutput.ts b/src/components/palette/editOutput.ts
index f09bfb166..3fb1998f2 100644
--- a/src/components/palette/editOutput.ts
+++ b/src/components/palette/editOutput.ts
@@ -21,6 +21,7 @@ import {
} from '../../parser/Symbols';
import { toExpression } from '../../parser/parseExpression';
import { getPlaceExpression } from '../../output/getOrCreatePlace';
+import type Spread from '../../nodes/Spread';
export function getNumber(given: Expression): number | undefined {
const measurement =
@@ -186,7 +187,7 @@ export function reviseContent(
db: Database,
project: Project,
list: ListLiteral,
- newValues: Expression[]
+ newValues: (Expression | Spread)[]
) {
db.Projects.revise(project, [[list, ListLiteral.make(newValues)]]);
}
diff --git a/src/examples/Adventure.wp b/src/examples/Adventure.wp
index a51e50c7b..6c3970441 100644
--- a/src/examples/Adventure.wp
+++ b/src/examples/Adventure.wp
@@ -21,8 +21,11 @@ Stage(
[
Group(
Stack()
- [Phrase(state.description duration: 0.5s name: state.action entering: Pose(offset: Place(0m 2m)))].append(options)
- )
+ [
+ Phrase(state.description duration: 0.5s name: state.action entering: Pose(offset: Place(0m 2m)))
+ :options
+ ]
+ )
]
background: Color(0 0 0%°)
color: Color(100% 0 0°)
diff --git a/src/examples/Hira.wp b/src/examples/Hira.wp
index 6718c2d8c..3e1c373b4 100644
--- a/src/examples/Hira.wp
+++ b/src/examples/Hira.wp
@@ -17,9 +17,7 @@ letters•[Output]: count → [].translate(
Stage(
- letters.append([
- Shape(Rectangle(-40m 0m 40m -2m))
- ])
+ [ :letters Shape(Rectangle(-40m 0m 40m -2m))]
gravity: gravity
place: Place(0m 5m -20m)
)
\ No newline at end of file
diff --git a/src/examples/Layers.wp b/src/examples/Layers.wp
index b1279767a..faa0aea2b 100644
--- a/src/examples/Layers.wp
+++ b/src/examples/Layers.wp
@@ -15,10 +15,13 @@ count: 10→[]
)
Stage(
- balls(0m).append(balls(-10m).append(balls(10m))).append([
+ [
+ :balls(0m)
+ :balls(-10m)
+ :balls(10m)
Shape(Rectangle(-20m 0m 20m -1m 0m))
Shape(Rectangle(-20m 0m 20m -1m 10m) rotation: 25°)
Shape(Rectangle(-20m 0m 20m -1m -10m) rotation: -10°)
- ])
+ ]
place: Place(0m 10m -30m)
)
\ No newline at end of file
diff --git a/src/examples/Questions.wp b/src/examples/Questions.wp
index 868c6968f..215f42caa 100644
--- a/src/examples/Questions.wp
+++ b/src/examples/Questions.wp
@@ -4,19 +4,22 @@ Questions
pressed: ∆ Key()
count•#: 0 … pressed … count + 1
-questions•[Question]: [] … pressed … questions.add(Question(count→"" Random(30 50) · 1m/s Random(30 50) · 1m/s Random(0 30) · 1°/s))
+questions•[Question]: [] … pressed … [:questions Question(count→"" Random(30 50) · 1m/s Random(30 50) · 1m/s Random(0 30) · 1°/s)]
Stage(
- [ Phrase('👨👨👧👦' place: Place(0m 0m) face: "Noto Emoji") Shape(Rectangle(-5m 0m 25m -1m))].append(questions.translate(
- ƒ(q•Question index•#) (
- initialize: pressed & (index = questions.length())
- Phrase(
- 'Q'
- name: q.id
- place: Motion(velocity: Velocity(q.vx q.vy q.va))
- matter: Matter()
- resting: Pose(opacity: initialize ? 0% 100%)
+ [
+ Phrase('👨👨👧👦' place: Place(0m 0m) face: "Noto Emoji") Shape(Rectangle(-5m 0m 25m -1m))
+ :questions.translate(
+ ƒ(q•Question index•#) (
+ initialize: pressed & (index = questions.length())
+ Phrase(
+ 'Q'
+ name: q.id
+ place: Motion(velocity: Velocity(q.vx q.vy q.va))
+ matter: Matter()
+ resting: Pose(opacity: initialize ? 0% 100%)
+ )
)
)
- ))
+ ]
)
\ No newline at end of file
diff --git a/src/locale/NodeTexts.ts b/src/locale/NodeTexts.ts
index c0427c451..ddd6d0a40 100644
--- a/src/locale/NodeTexts.ts
+++ b/src/locale/NodeTexts.ts
@@ -440,6 +440,11 @@ type NodeTexts = {
/** Placeholder label for an item in a list */
item: Template;
};
+ /**
+ * A way of spreading a list's values into a list literal, e.g., `[ [ 1 2 3]… 4 5]`
+ * Description inputs: none
+ */
+ Spread: NodeText;
/**
* A map literal, e.g., `{1:1 2:2 3:3}`
* Finish inputs: $1 = resulting value
diff --git a/src/locale/en-US.json b/src/locale/en-US.json
index 77272cf53..d4c9bf446 100644
--- a/src/locale/en-US.json
+++ b/src/locale/en-US.json
@@ -765,6 +765,14 @@
"finish": "I made a me! $1",
"item": "item"
},
+ "Spread": {
+ "name": "list spread",
+ "emotion": "serious",
+ "doc": [
+ "A help you make lists with the values of other lists. Like this:",
+ "\\list1: [1 2 3]\nlist2: [4 5 6]\nfinal: [list1… list2…]"
+ ]
+ },
"MapLiteral": {
"name": "map",
"description": "$1 pairing map",
@@ -1913,8 +1921,10 @@
},
"append": {
"doc": [
- "I create a new @List with my values, then all the values of the given @List.",
- "\\['apple' 'banana' 'mango'].append(['watermelon' 'starfruit'])\\"
+ "I create a new @List with my values, then all the values of the given @List after me.",
+ "\\['apple' 'banana' 'mango'].append(['watermelon' 'starfruit'])\\",
+ "It's a little bit easier to use @Spread though, like this:",
+ "\\['apple' 'banana' 'mango' :['watermelon' 'starfruit']]\\"
],
"names": ["append"],
"inputs": [
diff --git a/src/nodes/ListLiteral.ts b/src/nodes/ListLiteral.ts
index 5a307085d..5f80fe340 100644
--- a/src/nodes/ListLiteral.ts
+++ b/src/nodes/ListLiteral.ts
@@ -24,13 +24,15 @@ import type { BasisTypeName } from '../basis/BasisConstants';
import concretize from '../locale/concretize';
import Sym from './Sym';
import AnyType from './AnyType';
+import Spread from './Spread';
+import TypeException from '../values/TypeException';
export default class ListLiteral extends Expression {
readonly open: Token;
- readonly values: Expression[];
+ readonly values: (Spread | Expression)[];
readonly close?: Token;
- constructor(open: Token, values: Expression[], close?: Token) {
+ constructor(open: Token, values: (Spread | Expression)[], close?: Token) {
super();
this.open = open;
@@ -40,7 +42,7 @@ export default class ListLiteral extends Expression {
this.computeChildren();
}
- static make(values?: Expression[]) {
+ static make(values?: (Expression | Spread)[]) {
return new ListLiteral(
new ListOpenToken(),
values ?? [],
@@ -57,7 +59,7 @@ export default class ListLiteral extends Expression {
{ name: 'open', kind: node(Sym.ListOpen) },
{
name: 'values',
- kind: list(true, node(Expression)),
+ kind: list(true, node(Expression), node(Spread)),
label: (translation: Locale) =>
translation.node.ListLiteral.item,
// Only allow types to be inserted that are of the list's type, if provided.
@@ -74,7 +76,7 @@ export default class ListLiteral extends Expression {
clone(replace?: Replacement) {
return new ListLiteral(
this.replaceChild('open', this.open, replace),
- this.replaceChild('values', this.values, replace),
+ this.replaceChild('values', this.values, replace),
this.replaceChild('close', this.close, replace)
) as this;
}
@@ -117,7 +119,9 @@ export default class ListLiteral extends Expression {
}
getDependencies(): Expression[] {
- return [...this.values];
+ return this.values
+ .map((val) => (val instanceof Spread ? val.list : val))
+ .filter((val): val is Expression => val !== undefined);
}
compile(evaluator: Evaluator, context: Context): Step[] {
@@ -126,7 +130,11 @@ export default class ListLiteral extends Expression {
...this.values.reduce(
(steps: Step[], item) => [
...steps,
- ...item.compile(evaluator, context),
+ ...(item instanceof Spread
+ ? item.list
+ ? item.list.compile(evaluator, context)
+ : []
+ : item.compile(evaluator, context)),
],
[]
),
@@ -137,10 +145,34 @@ export default class ListLiteral extends Expression {
evaluate(evaluator: Evaluator, prior: Value | undefined): Value {
if (prior) return prior;
+ // Start with the list of values from the expression to help keep track of the ones that were handled.
+ const items = this.values.slice();
+
// Pop all of the values.
const values = [];
- for (let i = 0; i < this.values.length; i++)
- values.unshift(evaluator.popValue(this));
+ for (let i = 0; i < this.values.length; i++) {
+ const value = evaluator.popValue(this);
+ let item;
+ do {
+ item = items.pop();
+ } while (item instanceof Spread && item.list === undefined);
+ // Was this a spread value? Add all of its items to this list.
+ if (item instanceof Spread) {
+ if (value instanceof ListValue) {
+ // Add them in reverse order so they end up in the correct order.
+ for (let j = value.values.length - 1; j >= 0; j--)
+ values.unshift(value.values[j]);
+ } else
+ return new TypeException(
+ this,
+ evaluator,
+ ListType.make(),
+ value
+ );
+ }
+ // Add the non-spread value.
+ else values.unshift(value);
+ }
// Construct the new list.
return new ListValue(this, values);
diff --git a/src/nodes/Spread.ts b/src/nodes/Spread.ts
new file mode 100644
index 000000000..1e8606faf
--- /dev/null
+++ b/src/nodes/Spread.ts
@@ -0,0 +1,77 @@
+import Expression from './Expression';
+import type { Grammar, Replacement } from './Node';
+import Token from './Token';
+import type Locale from '@locale/Locale';
+import Glyphs from '../lore/Glyphs';
+import Purpose from '../concepts/Purpose';
+import type { BasisTypeName } from '../basis/BasisConstants';
+import Node, { node, optional } from './Node';
+import Sym from './Sym';
+import { BIND_SYMBOL } from '../parser/Symbols';
+import AnyType from './AnyType';
+import ListType from './ListType';
+
+/** Inside a list literal, flattens values of a list value into a new list */
+export default class Spread extends Node {
+ readonly dots: Token;
+ readonly list: Expression | undefined;
+
+ constructor(dots: Token, list: Expression | undefined) {
+ super();
+
+ this.dots = dots;
+ this.list = list;
+
+ this.computeChildren();
+ }
+
+ static make(list: Expression) {
+ return new Spread(new Token(BIND_SYMBOL, Sym.Bind), list);
+ }
+
+ static getPossibleNodes() {
+ return [];
+ }
+
+ getGrammar(): Grammar {
+ return [
+ {
+ name: 'dots',
+ kind: node(Sym.Bind),
+ },
+ {
+ name: 'list',
+ kind: optional(node(Expression)),
+ getType: () => ListType.make(new AnyType()),
+ label: (translation: Locale) => translation.term.list,
+ },
+ ];
+ }
+
+ clone(replace?: Replacement) {
+ return new Spread(
+ this.replaceChild('dots', this.dots, replace),
+ this.replaceChild('list', this.list, replace)
+ ) as this;
+ }
+
+ getPurpose(): Purpose {
+ return Purpose.Value;
+ }
+
+ getAffiliatedType(): BasisTypeName | undefined {
+ return 'list';
+ }
+
+ computeConflicts() {
+ return;
+ }
+
+ getNodeLocale(locale: Locale) {
+ return locale.node.Spread;
+ }
+
+ getGlyphs() {
+ return Glyphs.Stream;
+ }
+}
diff --git a/src/parser/parseExpression.ts b/src/parser/parseExpression.ts
index c5dcc8b38..0664d5d00 100644
--- a/src/parser/parseExpression.ts
+++ b/src/parser/parseExpression.ts
@@ -58,6 +58,7 @@ import { toTokens } from './toTokens';
import Docs from '../nodes/Docs';
import parseDoc from './parseDoc';
import type Doc from '../nodes/Doc';
+import Spread from '../nodes/Spread';
export function toExpression(code: string): Expression {
return parseExpression(toTokens(code));
@@ -451,18 +452,30 @@ function parseTranslation(tokens: Tokens): Translation {
return new Translation(text, segments, close, language);
}
-/** LIST :: [ EXPRESSION* ] */
+/** LIST :: [ (SPREAD|EXPRESSION)* ] */
function parseList(tokens: Tokens): ListLiteral {
const open = tokens.read(Sym.ListOpen);
- const values: Expression[] = [];
+ const values: (Spread | Expression)[] = [];
while (
tokens.hasNext() &&
tokens.nextIsnt(Sym.ListClose) &&
tokens.nextIsnt(Sym.Code) &&
!tokens.nextHasMoreThanOneLineBreak()
- )
- values.push(parseExpression(tokens));
+ ) {
+ // Is there a spread next? Parse it.
+ if (tokens.nextIs(Sym.Bind)) {
+ const dots = tokens.read(Sym.Bind);
+ const value =
+ tokens.hasNext() &&
+ tokens.nextIsnt(Sym.ListClose) &&
+ tokens.nextIsnt(Sym.Code) &&
+ !tokens.nextHasMoreThanOneLineBreak()
+ ? parseExpression(tokens)
+ : undefined;
+ values.push(new Spread(dots, value));
+ } else values.push(parseExpression(tokens));
+ }
const close = tokens.readIf(Sym.ListClose);
diff --git a/static/locales/es-MX/es-MX.json b/static/locales/es-MX/es-MX.json
index 66f0a7dd5..59917d34f 100644
--- a/static/locales/es-MX/es-MX.json
+++ b/static/locales/es-MX/es-MX.json
@@ -492,6 +492,11 @@
"finish": "$?",
"item": "$?"
},
+ "Spread": {
+ "name": "$?",
+ "emotion": "serious",
+ "doc": "$?"
+ },
"MapLiteral": {
"name": "índice",
"description": "$?",
diff --git a/static/locales/example/example.json b/static/locales/example/example.json
index a92fa729e..e530368c2 100644
--- a/static/locales/example/example.json
+++ b/static/locales/example/example.json
@@ -492,6 +492,11 @@
"finish": "$?",
"item": "$?"
},
+ "Spread": {
+ "name": "$?",
+ "emotion": "serious",
+ "doc": "$?"
+ },
"MapLiteral": {
"name": "$?",
"description": "$?",
diff --git a/static/locales/zh-CN/zh-CN.json b/static/locales/zh-CN/zh-CN.json
index 48252818a..88248e021 100644
--- a/static/locales/zh-CN/zh-CN.json
+++ b/static/locales/zh-CN/zh-CN.json
@@ -492,6 +492,11 @@
"finish": "$?",
"item": "$?"
},
+ "Spread": {
+ "name": "$?",
+ "emotion": "serious",
+ "doc": "$?"
+ },
"MapLiteral": {
"name": "$?",
"description": "$?",
diff --git a/static/schemas/Locale.json b/static/schemas/Locale.json
index e8a722159..5776079d5 100644
--- a/static/schemas/Locale.json
+++ b/static/schemas/Locale.json
@@ -5255,6 +5255,10 @@
"$ref": "#/definitions/NodeText",
"description": "A source file that contains a name and program."
},
+ "Spread": {
+ "$ref": "#/definitions/NodeText",
+ "description": "A way of spreading a list's values into a list literal, e.g., `[ [ 1 2 3]… 4 5]` Description inputs: none"
+ },
"StreamDefinition": {
"additionalProperties": false,
"description": "A stream definition. Not typically written, since all streams are defined internally, but basically like a structure definition, e.g., `… Key()`",
@@ -6081,6 +6085,7 @@
"IsLocale",
"ListAccess",
"ListLiteral",
+ "Spread",
"MapLiteral",
"NumberLiteral",
"InternalExpression",
diff --git a/static/schemas/Tutorial.json b/static/schemas/Tutorial.json
index abb1a8f5e..da4d9582b 100644
--- a/static/schemas/Tutorial.json
+++ b/static/schemas/Tutorial.json
@@ -68,6 +68,7 @@
"IsLocale",
"ListAccess",
"ListLiteral",
+ "Spread",
"MapLiteral",
"NumberLiteral",
"InternalExpression",