Repository: syuilo/aiscript (ts)

Issue:
プリミティブ型のメソッド
```
let a = [1 2 3]
let b = a.map((x) =>  x + 1)
```
とかやりたい

現状:
```
let a = [1 2 3]
let b = Arr:map(a, (x) =>  x + 1)
```

Pull request description:
Primitive methods
<!-- ℹ お読みください
PRありがとうございます！ PRを作成する前に、以下をご確認ください:
- 可能であればタイトルに、以下で示すようなPRの種類が分かるキーワードをプリフィクスしてください。
  - fix / refactor / feat / enhance / perf / chore
  - また、PRの粒度が適切であることを確認してください。ひとつのPRに複数の種類の変更や関心を含めることは避けてください。
- このPRによって解決されるIssueがある場合は、そのIssueへの参照を本文内に含めてください。
- CHANGELOG.mdに変更点を追記してください。リファクタリングなど、利用者に影響を与えない変更についてはこの限りではありません。
- この変更により新たに作成、もしくは更新すべきドキュメントがないか確認してください。
- 機能追加やバグ修正をした場合は、可能であればテストケースを追加してください。
- テスト、Lintが通っていることを予め確認してください。
  - `npm run test`、`npm run lint`でぞれぞれ実施可能です
- `npm run api`を実行してAPIレポートを更新し、差分がある場合はコミットしてください。
ご協力ありがとうございます🤗
-->
<!-- ℹ README
Thank you for your PR! Before creating a PR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
  - fix / refactor / feat / enhance / perf / chore
  - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text.
- Please add the summary of the changes to CHANGELOG.md. However, this is not necessary for changes that do not affect the users, such as refactoring.
- Check if there are any documents that need to be created or updated due to this change.
- If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that tests and Lint are passed in advance.
  - You can run it with `npm run test` and `npm run lint`.
- Run `npm run api` to update the API report and commit it if there are any diffs.
Thanks for your cooperation 🤗
-->

# What
<!-- このPRで何をしたのか？ どう変わるのか？ -->
<!-- What did you do with this PR? How will it change things? -->
Resolve #208

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3cd669..e440042 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,10 @@
 - for、およびeachは配列を返さなくなりました
 - ブロック式は`{ }`→`eval { }`に
 - 配列のインデックスは0始まりになりました
+- いくつかのstdに含まれるメソッドは、対象の値のプロパティとして利用するようになりました。例:
+	- `Str:to_num("123")` -> `"123".to_num()`
+	- `Arr:len([1 2 3])` -> `[1 2 3].len`
+	- etc
 
 ## Features
 - `continue`
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 75892d7..14c764f 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -7,9 +7,10 @@ import * as N from '../node';
 import { Scope } from './scope';
 import { AiScriptError } from './error';
 import { std } from './lib/std';
-import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, isString, expectAny } from './util';
+import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, isString, expectAny, isNumber } from './util';
 import { Value, NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, VFn, BREAK, CONTINUE } from './value';
 import { infixToFnCall } from './infix-to-fncall';
+import { PRIMITIVE_PROPS } from './primitive-props';
 
 export class AiScript {
 	private vars: Record<string, Value>;
@@ -363,12 +364,33 @@ export class AiScript {
 			}
 
 			case 'prop': {
-				const obj = await this._eval(node.target, scope);
-				assertObject(obj);
-				if (obj.value.has(node.name)) {
-					return obj.value.get(node.name)!;
+				const target = await this._eval(node.target, scope);
+				if (isObject(target)) {
+					if (target.value.has(node.name)) {
+						return target.value.get(node.name)!;
+					} else {
+						return NULL;
+					}
+				} else if (isNumber(target)) {
+					if (Object.hasOwn(PRIMITIVE_PROPS.num, node.name)) {
+						return PRIMITIVE_PROPS.num[node.name as keyof typeof PRIMITIVE_PROPS['num']](target);
+					} else {
+						throw new AiScriptError(`No such prop (${node.name}) in ${target.type}.`);
+					}
+				} else if (isString(target)) {
+					if (Object.hasOwn(PRIMITIVE_PROPS.str, node.name)) {
+						return PRIMITIVE_PROPS.str[node.name as keyof typeof PRIMITIVE_PROPS['str']](target);
+					} else {
+						throw new AiScriptError(`No such prop (${node.name}) in ${target.type}.`);
+					}
+				} else if (isArray(target)) {
+					if (Object.hasOwn(PRIMITIVE_PROPS.arr, node.name)) {
+						return PRIMITIVE_PROPS.arr[node.name as keyof typeof PRIMITIVE_PROPS['arr']](target);
+					} else {
+						throw new AiScriptError(`No such prop (${node.name}) in ${target.type}.`);
+					}
 				} else {
-					return NULL; // エラーにしてもよさそう
+					throw new AiScriptError(`Cannot read prop (${node.name}) of ${target.type}.`);
 				}
 			}
 
diff --git a/src/interpreter/lib/std.ts b/src/interpreter/lib/std.ts
index 85a15f3..959f55d 100644
--- a/src/interpreter/lib/std.ts
+++ b/src/interpreter/lib/std.ts
@@ -260,206 +260,9 @@ export const std: Record<string, Value> = {
 
 	//#region Str
 	'Str:lf': STR('\n'),
-
-	'Str:to_num': FN_NATIVE(([v]) => {
-		expectAny(v);
-		if (v.type === 'num') return v;
-		if (v.type === 'str') {
-			const parsed = parseInt(v.value, 10);
-			if (isNaN(parsed)) return NULL;
-			return NUM(parsed);
-		}
-		return NULL;
-	}),
-
-	'Str:len': FN_NATIVE(([v]) => {
-		expectAny(v);
-		if (v.type !== 'str') return NUM(0);
-		return NUM(length(v.value));
-	}),
-
-	'Str:pick': FN_NATIVE(([v, i]) => {
-		assertString(v);
-		assertNumber(i);
-		const chars = toArray(v.value);
-		const char = chars[i.value - 1];
-		return char ? STR(char) : NULL;
-	}),
-
-	'Str:incl': FN_NATIVE(([v, keyword]) => {
-		assertString(v);
-		assertString(keyword);
-		return v.value.includes(keyword.value) ? TRUE : FALSE;
-	}),
-
-	'Str:slice': FN_NATIVE(([v, begin, end]) => {
-		assertString(v);
-		assertNumber(begin);
-		assertNumber(end);
-		return STR(substring(v.value, begin.value - 1, end.value - 1));
-	}),
-
-	'Str:split': FN_NATIVE(([v, splitter]) => {
-		assertString(v);
-		if (splitter) assertString(splitter);
-		if (splitter) {
-			return ARR(v.value.split(splitter ? splitter.value : '').map(s => STR(s)));
-		} else {
-			return ARR(toArray(v.value).map(s => STR(s)));
-		}
-	}),
-
-	'Str:replace': FN_NATIVE(([v, a, b]) => {
-		assertString(v);
-		assertString(a);
-		assertString(b);
-		return STR(v.value.split(a.value).join(b.value));
-	}),
-
-	'Str:index_of': FN_NATIVE(([v, search]) => {
-		assertString(v);
-		assertString(search);
-		return NUM(indexOf(v.value, search.value) + 1);
-	}),
-
-	'Str:trim': FN_NATIVE(([v]) => {
-		assertString(v);
-		return STR(v.value.trim());
-	}),
-
-	'Str:upper': FN_NATIVE(([v]) => {
-		assertString(v);
-		return STR(v.value.toUpperCase());
-	}),
-
-	'Str:lower': FN_NATIVE(([v]) => {
-		assertString(v);
-		return STR(v.value.toLowerCase());
-	}),
 	//#endregion
 
 	//#region Arr
-	'Arr:len': FN_NATIVE(([arr]) => {
-		expectAny(arr);
-		if (arr.type !== 'arr') return NUM(0);
-		return NUM(arr.value.length);
-	}),
-
-	'Arr:push': FN_NATIVE(([arr, val]) => {
-		assertArray(arr);
-		expectAny(val);
-		arr.value.push(val);
-		return NULL;
-	}),
-
-	'Arr:unshift': FN_NATIVE(([arr, val]) => {
-		assertArray(arr);
-		expectAny(val);
-		arr.value.unshift(val);
-		return NULL;
-	}),
-
-	'Arr:pop': FN_NATIVE(([arr]) => {
-		assertArray(arr);
-		return arr.value.pop();
-	}),
-
-	'Arr:shift': FN_NATIVE(([arr]) => {
-		assertArray(arr);
-		return arr.value.shift();
-	}),
-
-	'Arr:concat': FN_NATIVE(([a, b]) => {
-		assertArray(a);
-		assertArray(b);
-		return ARR(a.value.concat(b.value));
-	}),
-
-	'Arr:join': FN_NATIVE(([arr, joiner]) => {
-		assertArray(arr);
-		if (joiner) assertString(joiner);
-		return STR(arr.value.map(i => i.type === 'str' ? i.value : '').join(joiner ? joiner.value : ''));
-	}),
-
-	'Arr:slice': FN_NATIVE(([arr, begin, end]) => {
-		assertArray(arr);
-		assertNumber(begin);
-		assertNumber(end);
-		return ARR(arr.value.slice(begin.value - 1, end.value - 1));
-	}),
-
-	'Arr:incl': FN_NATIVE(([arr, val]) => {
-		assertArray(arr);
-		expectAny(val);
-		if (val.type !== 'str' && val.type !== 'num' && val.type !== 'bool' && val.type !== 'null') return FALSE;
-		const getValue = (v: VArr) => {
-			return v.value.map(i => {
-				if (i.type === 'str') return i.value;
-				if (i.type === 'num') return i.value;
-				if (i.type === 'bool') return i.value;
-				if (i.type === 'null') return null;
-				return Symbol();
-			});
-		};
-		return getValue(arr).includes(val.type === 'null' ? null : val.value) ? TRUE : FALSE;
-	}),
-
-	'Arr:map': FN_NATIVE(async ([arr, fn], opts) => {
-		assertArray(arr);
-		assertFunction(fn);
-		const vals = arr.value.map(async (item, i) => {
-			return await opts.call(fn, [item, NUM(i + 1)]);
-		});
-		return ARR(await Promise.all(vals));
-	}),
-
-	'Arr:filter': FN_NATIVE(async ([arr, fn], opts) => {
-		assertArray(arr);
-		assertFunction(fn);
-		const vals = [] as Value[];
-		for (let i = 0; i < arr.value.length; i++) {
-			const item = arr.value[i];
-			const res = await opts.call(fn, [item, NUM(i + 1)]);
-			assertBoolean(res);
-			if (res.value) vals.push(item);
-		}
-		return ARR(vals);
-	}),
-
-	'Arr:reduce': FN_NATIVE(async ([arr, fn, initialValue], opts) => {
-		assertArray(arr);
-		assertFunction(fn);
-		const withInitialValue = initialValue != null;
-		let accumulator = withInitialValue ? initialValue : arr.value[0];
-		for (let i = withInitialValue ? 0 : 1; i < arr.value.length; i++) {
-			const item = arr.value[i];
-			accumulator = await opts.call(fn, [accumulator, item, NUM(i + 1)]);
-		}
-		return accumulator;
-	}),
-
-	'Arr:find': FN_NATIVE(async ([arr, fn], opts) => {
-		assertArray(arr);
-		assertFunction(fn);
-		for (let i = 0; i < arr.value.length; i++) {
-			const item = arr.value[i];
-			const res = await opts.call(fn, [item, NUM(i + 1)]);
-			assertBoolean(res);
-			if (res.value) return item;
-		}
-		return NULL;
-	}),
-
-	'Arr:reverse': FN_NATIVE(([arr]) => {
-		assertArray(arr);
-		arr.value.reverse();
-		return NULL;
-	}),
-
-	'Arr:copy': FN_NATIVE(async ([arr]) => {
-		assertArray(arr);
-		return ARR([...arr.value]);
-	}),
 	//#endregion
 
 	//#region Obj
diff --git a/src/interpreter/primitive-props.ts b/src/interpreter/primitive-props.ts
new file mode 100644
index 0000000..fe9fe41
--- /dev/null
+++ b/src/interpreter/primitive-props.ts
@@ -0,0 +1,177 @@
+import { substring, length, indexOf, toArray } from 'stringz';
+import { assertArray, assertBoolean, assertFunction, assertNumber, assertString, expectAny } from './util';
+import { ARR, FALSE, FN_NATIVE, NULL, NUM, STR, TRUE, Value, VArr, VFn, VNum, VStr } from './value';
+
+export const PRIMITIVE_PROPS = {
+	num: {
+		to_str: (target: VNum): VFn => FN_NATIVE(async ([], opts) => {
+			return STR(target.value.toString());
+		}),
+	},
+
+	str: {
+		to_num: (target: VStr): VFn => FN_NATIVE(async ([], opts) => {
+			const parsed = parseInt(target.value, 10);
+			if (isNaN(parsed)) return NULL;
+			return NUM(parsed);
+		}),
+
+		len: (target: VStr): VNum => NUM(length(target.value)),
+
+		replace: (target: VStr): VFn => FN_NATIVE(async ([a, b], opts) => {
+			assertString(a);
+			assertString(b);
+			return STR(target.value.split(a.value).join(b.value));
+		}),
+
+		index_of: (target: VStr): VFn => FN_NATIVE(async ([search], opts) => {
+			assertString(search);
+			return NUM(indexOf(target.value, search.value));
+		}),
+
+		incl: (target: VStr): VFn => FN_NATIVE(async ([search], opts) => {
+			assertString(search);
+			return target.value.includes(search.value) ? TRUE : FALSE;
+		}),
+
+		trim: (target: VStr): VFn => FN_NATIVE(async ([], opts) => {
+			return STR(target.value.trim());
+		}),
+
+		upper: (target: VStr): VFn => FN_NATIVE(async ([], opts) => {
+			return STR(target.value.toUpperCase());
+		}),
+
+		lower: (target: VStr): VFn => FN_NATIVE(async ([], opts) => {
+			return STR(target.value.toLowerCase());
+		}),
+
+		split: (target: VStr): VFn => FN_NATIVE(async ([splitter], opts) => {
+			if (splitter) assertString(splitter);
+			if (splitter) {
+				return ARR(target.value.split(splitter ? splitter.value : '').map(s => STR(s)));
+			} else {
+				return ARR(toArray(target.value).map(s => STR(s)));
+			}
+		}),
+
+		slice: (target: VStr): VFn => FN_NATIVE(async ([begin, end], opts) => {
+			assertNumber(begin);
+			assertNumber(end);
+			return STR(substring(target.value, begin.value, end.value));
+		}),
+
+		pick: (target: VStr): VFn => FN_NATIVE(async ([i], opts) => {
+			assertNumber(i);
+			const chars = toArray(target.value);
+			const char = chars[i.value];
+			return char ? STR(char) : NULL;
+		}),
+	},
+
+	arr: {
+		len: (target: VArr): VNum => NUM(target.value.length),
+
+		push: (target: VArr): VFn => FN_NATIVE(async ([val], opts) => {
+			expectAny(val);
+			target.value.push(val);
+			return target;
+		}),
+
+		unshift: (target: VArr): VFn => FN_NATIVE(async ([val], opts) => {
+			expectAny(val);
+			target.value.unshift(val);
+			return target;
+		}),
+
+		pop: (target: VArr): VFn => FN_NATIVE(async ([], opts) => {
+			return target.value.pop() ?? NULL;
+		}),
+
+		shift: (target: VArr): VFn => FN_NATIVE(async ([], opts) => {
+			return target.value.shift() ?? NULL;
+		}),
+
+		concat: (target: VArr): VFn => FN_NATIVE(async ([x], opts) => {
+			assertArray(x);
+			return ARR(target.value.concat(x.value));
+		}),
+
+		slice: (target: VArr): VFn => FN_NATIVE(async ([begin, end], opts) => {
+			assertNumber(begin);
+			assertNumber(end);
+			return ARR(target.value.slice(begin.value, end.value));
+		}),
+
+		join: (target: VArr): VFn => FN_NATIVE(async ([joiner], opts) => {
+			if (joiner) assertString(joiner);
+			return STR(target.value.map(i => i.type === 'str' ? i.value : '').join(joiner ? joiner.value : ''));
+		}),
+
+		map: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => {
+			assertFunction(fn);
+			const vals = target.value.map(async (item, i) => {
+				return await opts.call(fn, [item, NUM(i + 1)]);
+			});
+			return ARR(await Promise.all(vals));
+		}),
+
+		filter: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => {
+			assertFunction(fn);
+			const vals = [] as Value[];
+			for (let i = 0; i < target.value.length; i++) {
+				const item = target.value[i];
+				const res = await opts.call(fn, [item, NUM(i + 1)]);
+				assertBoolean(res);
+				if (res.value) vals.push(item);
+			}
+			return ARR(vals);
+		}),
+
+		reduce: (target: VArr): VFn => FN_NATIVE(async ([fn, initialValue], opts) => {
+			assertFunction(fn);
+			const withInitialValue = initialValue != null;
+			let accumulator = withInitialValue ? initialValue : target.value[0];
+			for (let i = withInitialValue ? 0 : 1; i < target.value.length; i++) {
+				const item = target.value[i];
+				accumulator = await opts.call(fn, [accumulator, item, NUM(i + 1)]);
+			}
+			return accumulator;
+		}),
+
+		find: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => {
+			assertFunction(fn);
+			for (let i = 0; i < target.value.length; i++) {
+				const item = target.value[i];
+				const res = await opts.call(fn, [item, NUM(i + 1)]);
+				assertBoolean(res);
+				if (res.value) return item;
+			}
+			return NULL;
+		}),
+
+		incl: (target: VArr): VFn => FN_NATIVE(async ([val], opts) => {
+			expectAny(val);
+			if (val.type !== 'str' && val.type !== 'num' && val.type !== 'bool' && val.type !== 'null') return FALSE;
+			const getValue = (v: VArr) => {
+				return v.value.map(i => {
+					if (i.type === 'str') return i.value;
+					if (i.type === 'num') return i.value;
+					if (i.type === 'bool') return i.value;
+					if (i.type === 'null') return null;
+					return Symbol();
+				});
+			};
+			return getValue(target).includes(val.type === 'null' ? null : val.value) ? TRUE : FALSE;
+		}),
+
+		reverse: (target: VArr): VFn => FN_NATIVE(async ([], opts) => {
+			target.value.reverse();
+			return NULL;
+		}),
+
+		copy: (target: VArr): VFn => FN_NATIVE(async ([], opts) => {
+			return ARR([...target.value]);
+		}),
+	},
+} as const;
diff --git a/tsconfig.json b/tsconfig.json
index b04fedc..8dd0e38 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,7 +9,7 @@
 		"declaration": true,
 		"sourceMap": true,
 		"declarationMap": true,
-		"target": "es2017",
+		"target": "es2022",
 		"module": "ES2020",
 		"moduleResolution": "node",
 		"esModuleInterop": true,


Test patch:
diff --git a/test/index.ts b/test/index.ts
index c363dd4..9057b5f 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -7,7 +7,7 @@ import * as assert from 'assert';
 import { Parser, utils } from '../src';
 import { AiScript } from '../src/interpreter';
 import { AiScriptError } from '../src/interpreter/error';
-import { NUM, STR, NULL, ARR, OBJ, BOOL } from '../src/interpreter/value';
+import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE } from '../src/interpreter/value';
 import * as N from '../src/node';
 
 const exe = (program: string): Promise<any> => new Promise((ok, err) => {
@@ -228,7 +228,7 @@ it('//', async () => {
 it('式にコロンがあってもオブジェクトと判定されない', async () => {
 	const res = await exe(`
 	<: eval {
-		Str:incl("ai", "a")
+		Core:eq("ai", "ai")
 	}
 	`);
 	eq(res, BOOL(true));
@@ -855,7 +855,7 @@ describe('chain', () => {
 		let x = { a: ["ai", "chan", "kawaii"] }
 		each let item, x.a {
 			let y = { a: item }
-			Arr:push(msgs, Arr:join([y.a, "!"]))
+			msgs.push([y.a, "!"].join())
 		}
 		<: msgs
 		`);
@@ -1294,10 +1294,10 @@ describe('loop', () => {
 		var a = ["ai" "chan" "kawaii" "!"]
 		var b = []
 		loop {
-			var x = Arr:shift(a)
+			var x = a.shift()
 			if (x == "chan") continue
 			if (x == "!") break
-			Arr:push(b, x)
+			b.push(x)
 		}
 		<: b
 		`);
@@ -1378,7 +1378,7 @@ describe('for of', () => {
 		const res = await exe(`
 		let msgs = []
 		each let item, ["ai", "chan", "kawaii"] {
-			Arr:push(msgs, Arr:join([item, "!"]))
+			msgs.push([item, "!"].join())
 		}
 		<: msgs
 		`);
@@ -1390,7 +1390,7 @@ describe('for of', () => {
 		let msgs = []
 		each let item, ["ai", "chan", "kawaii"] {
 			if (item == "kawaii") break
-			Arr:push(msgs, Arr:join([item, "!"]))
+			msgs.push([item, "!"].join())
 		}
 		<: msgs
 		`);
@@ -1400,7 +1400,7 @@ describe('for of', () => {
 	it('single statement', async () => {
 		const res = await exe(`
 		let msgs = []
-		each let item, ["ai", "chan", "kawaii"] Arr:push(msgs, Arr:join([item, "!"]))
+		each let item, ["ai", "chan", "kawaii"] msgs.push([item, "!"].join())
 		<: msgs
 		`);
 		eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')]));
@@ -1936,96 +1936,322 @@ describe('Location', () => {
 	});
 });
 
-describe('std', () => {
-	describe('Arr', () => {
-		it('map', async () => {
+describe('primitive props', () => {
+	describe('num', () => {
+		it('to_str', async () => {
 			const res = await exe(`
-			let arr = ["ai", "chan", "kawaii"]
+			let num = 123
+			<: num.to_str()
+			`);
+			eq(res, STR('123'));
+		});
+	});
 
-			<: Arr:map(arr, @(item) { Arr:join([item, "!"]) })
+	describe('str', () => {
+		it('len', async () => {
+			const res = await exe(`
+			let str = "hello"
+			<: str.len
 			`);
-			eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')]));
+			eq(res, NUM(5));
 		});
 
-		it('filter', async () => {
+		it('to_num', async () => {
 			const res = await exe(`
-			let arr = ["ai", "chan", "kawaii"]
+			let str = "123"
+			<: str.to_num()
+			`);
+			eq(res, NUM(123));
+		});
 
-			<: Arr:filter(arr, @(item) { Str:incl(item, "ai") })
+		it('upper', async () => {
+			const res = await exe(`
+			let str = "hello"
+			<: str.upper()
 			`);
-			eq(res, ARR([STR('ai'), STR('kawaii')]));
+			eq(res, STR('HELLO'));
 		});
 
-		it('reduce', async () => {
+		it('lower', async () => {
 			const res = await exe(`
-			let arr = [1, 2, 3, 4]
+			let str = "HELLO"
+			<: str.lower()
+			`);
+			eq(res, STR('hello'));
+		});
 
-			<: Arr:reduce(arr, @(accumulator, currentValue) { (accumulator + currentValue) })
+		it('trim', async () => {
+			const res = await exe(`
+			let str = " hello  "
+			<: str.trim()
 			`);
-			eq(res, NUM(10));
+			eq(res, STR('hello'));
 		});
-	});
 
-	describe('Obj', () => {
-		it('keys', async () => {
+		it('replace', async () => {
 			const res = await exe(`
-			let o = { a: 1; b: 2; c: 3; }
+			let str = "hello"
+			<: str.replace("l", "x")
+			`);
+			eq(res, STR('hexxo'));
+		});
 
-			<: Obj:keys(o)
+		it('index_of', async () => {
+			const res = await exe(`
+			let str = "hello"
+			<: str.index_of("l")
+			`);
+			eq(res, NUM(2));
+		});
+
+		it('incl', async () => {
+			const res = await exe(`
+			let str = "hello"
+			<: [str.incl("ll"), str.incl("x")]
+			`);
+			eq(res, ARR([TRUE, FALSE]));
+		});
+
+		it('split', async () => {
+			const res = await exe(`
+			let str = "a,b,c"
+			<: str.split(",")
 			`);
 			eq(res, ARR([STR('a'), STR('b'), STR('c')]));
 		});
 
-		it('kvs', async () => {
+		it('pick', async () => {
 			const res = await exe(`
-			let o = { a: 1; b: 2; c: 3; }
+			let str = "hello"
+			<: str.pick(1)
+			`);
+			eq(res, STR('e'));
+		});
 
-			<: Obj:kvs(o)
+		it('slice', async () => {
+			const res = await exe(`
+			let str = "hello"
+			<: str.slice(1, 3)
 			`);
-			eq(res, ARR([
-				ARR([STR('a'), NUM(1)]),
-				ARR([STR('b'), NUM(2)]),
-				ARR([STR('c'), NUM(3)])
-			]));
+			eq(res, STR('el'));
 		});
 	});
 
-	describe('Str', () => {
+	describe('arr', () => {
 		it('len', async () => {
 			const res = await exe(`
-			<: Str:len("👍🏽🍆🌮")
+			let arr = [1, 2, 3]
+			<: arr.len
 			`);
 			eq(res, NUM(3));
 		});
 
-		it('pick', async () => {
+		it('push', async () => {
+			const res = await exe(`
+			let arr = [1, 2, 3]
+			arr.push(4)
+			<: arr
+			`);
+			eq(res, ARR([NUM(1), NUM(2), NUM(3), NUM(4)]));
+		});
+
+		it('unshift', async () => {
+			const res = await exe(`
+			let arr = [1, 2, 3]
+			arr.unshift(4)
+			<: arr
+			`);
+			eq(res, ARR([NUM(4), NUM(1), NUM(2), NUM(3)]));
+		});
+
+		it('pop', async () => {
+			const res = await exe(`
+			let arr = [1, 2, 3]
+			let popped = arr.pop()
+			<: [popped, arr]
+			`);
+			eq(res, ARR([NUM(3), ARR([NUM(1), NUM(2)])]));
+		});
+
+		it('shift', async () => {
+			const res = await exe(`
+			let arr = [1, 2, 3]
+			let shifted = arr.shift()
+			<: [shifted, arr]
+			`);
+			eq(res, ARR([NUM(1), ARR([NUM(2), NUM(3)])]));
+		});
+
+		it('concat', async () => {
 			const res = await exe(`
-			<: Str:pick("👍🏽🍆🌮", 2)
+			let arr = [1, 2, 3]
+			let concated = arr.concat([4, 5])
+			<: [concated, arr]
 			`);
-			eq(res, STR('🍆'));
+			eq(res, ARR([
+				ARR([NUM(1), NUM(2), NUM(3), NUM(4), NUM(5)]),
+				ARR([NUM(1), NUM(2), NUM(3)])
+			]));
 		});
 
 		it('slice', async () => {
 			const res = await exe(`
-			<: Str:slice("Emojis 👍🏽 are 🍆 poison. 🌮s are bad.", 8, 15)
+			let arr = ["ant", "bison", "camel", "duck", "elephant"]
+			let sliced = arr.slice(2, 4)
+			<: [sliced, arr]
+			`);
+			eq(res, ARR([
+				ARR([STR('camel'), STR('duck')]),
+				ARR([STR('ant'), STR('bison'), STR('camel'), STR('duck'), STR('elephant')])
+			]));
+		});
+
+		it('join', async () => {
+			const res = await exe(`
+			let arr = ["a", "b", "c"]
+			<: arr.join("-")
 			`);
-			eq(res, STR('👍🏽 are 🍆'));
+			eq(res, STR('a-b-c'));
 		});
 
-		it('split', async () => {
+		it('map', async () => {
+			const res = await exe(`
+			let arr = [1, 2, 3]
+			<: arr.map(@(item) { item * 2 })
+			`);
+			eq(res, ARR([NUM(2), NUM(4), NUM(6)]));
+		});
+
+		it('filter', async () => {
+			const res = await exe(`
+			let arr = [1, 2, 3]
+			<: arr.filter(@(item) { item != 2 })
+			`);
+			eq(res, ARR([NUM(1), NUM(3)]));
+		});
+
+		it('reduce', async () => {
+			const res = await exe(`
+			let arr = [1, 2, 3, 4]
+			<: arr.reduce(@(accumulator, currentValue) { (accumulator + currentValue) })
+			`);
+			eq(res, NUM(10));
+		});
+
+		it('find', async () => {
+			const res = await exe(`
+			let arr = ["abc", "def", "ghi"]
+			<: arr.find(@(item) { item.incl("e") })
+			`);
+			eq(res, STR('def'));
+		});
+
+		it('incl', async () => {
+			const res = await exe(`
+			let arr = ["abc", "def", "ghi"]
+			<: [arr.incl("def"), arr.incl("jkl")]
+			`);
+			eq(res, ARR([TRUE, FALSE]));
+		});
+
+		it('reverse', async () => {
 			const res = await exe(`
-			<: Str:split("👍🏽🍆🌮")
+			let arr = [1, 2, 3]
+			arr.reverse()
+			<: arr
 			`);
-			eq(res, ARR([STR('👍🏽'), STR('🍆'), STR('🌮')]));
+			eq(res, ARR([NUM(3), NUM(2), NUM(1)]));
 		});
 
+		it('copy', async () => {
+			const res = await exe(`
+			let arr = [1, 2, 3]
+			let copied = arr.copy()
+			copied.reverse()
+			<: [copied, arr]
+			`);
+			eq(res, ARR([
+				ARR([NUM(3), NUM(2), NUM(1)]),
+				ARR([NUM(1), NUM(2), NUM(3)])
+			]));
+		});
+	});
+});
 
+describe('std', () => {
+	describe('Core', () => {
 		it('range', async () => {
 			eq(await exe('<: Core:range(1, 10)'), ARR([NUM(1), NUM(2), NUM(3), NUM(4), NUM(5), NUM(6), NUM(7), NUM(8), NUM(9), NUM(10)]));
 			eq(await exe('<: Core:range(1, 1)'), ARR([NUM(1)]));
 			eq(await exe('<: Core:range(9, 7)'), ARR([NUM(9), NUM(8), NUM(7)]));
 		});
 	});
+
+	describe('Arr', () => {
+	});
+
+	describe('Obj', () => {
+		it('keys', async () => {
+			const res = await exe(`
+			let o = { a: 1; b: 2; c: 3; }
+
+			<: Obj:keys(o)
+			`);
+			eq(res, ARR([STR('a'), STR('b'), STR('c')]));
+		});
+
+		it('kvs', async () => {
+			const res = await exe(`
+			let o = { a: 1; b: 2; c: 3; }
+
+			<: Obj:kvs(o)
+			`);
+			eq(res, ARR([
+				ARR([STR('a'), NUM(1)]),
+				ARR([STR('b'), NUM(2)]),
+				ARR([STR('c'), NUM(3)])
+			]));
+		});
+	});
+
+	describe('Str', () => {
+		it('lf', async () => {
+			const res = await exe(`
+			<: Str:lf
+			`);
+			eq(res, STR('\n'));
+		});
+	});
+});
+
+describe('Unicode', () => {
+	it('len', async () => {
+		const res = await exe(`
+		<: "👍🏽🍆🌮".len
+		`);
+		eq(res, NUM(3));
+	});
+
+	it('pick', async () => {
+		const res = await exe(`
+		<: "👍🏽🍆🌮".pick(1)
+		`);
+		eq(res, STR('🍆'));
+	});
+
+	it('slice', async () => {
+		const res = await exe(`
+		<: "Emojis 👍🏽 are 🍆 poison. 🌮s are bad.".slice(7, 14)
+		`);
+		eq(res, STR('👍🏽 are 🍆'));
+	});
+
+	it('split', async () => {
+		const res = await exe(`
+		<: "👍🏽🍆🌮".split()
+		`);
+		eq(res, ARR([STR('👍🏽'), STR('🍆'), STR('🌮')]));
+	});
 });
 
 describe('extra', () => {
@@ -2038,7 +2264,7 @@ describe('extra', () => {
 				elif (i % 3 == 0) "Fizz"
 				elif (i % 5 == 0) "Buzz"
 				else i
-			Arr:push(res msg)
+			res.push(msg)
 		}
 		<: res
 		`);
@@ -2073,8 +2299,6 @@ describe('extra', () => {
 
 		// combine
 		@c(l) {
-			let L = Arr:len(l)
-
 			// extract
 			@x(v) {
 				if (Core:type(v) == "arr") { c(v) } else { v }
@@ -2082,7 +2306,7 @@ describe('extra', () => {
 
 			// rec
 			@r(f, n) {
-				if (n < L) {
+				if (n < l.len) {
 					r(f(x(l[n])), (n + 1))
 				} else { f }
 			}