diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index e2e714e0..0a912e4f 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.5 - name: Setup Node.js uses: actions/setup-node@v4.0.2 diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 00064d0f..d1b88410 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.5 - name: Setup Node.js uses: actions/setup-node@v4.0.2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea4397af..a3eb5538 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.5 - name: Setup Node.js uses: actions/setup-node@v4.0.2 diff --git a/.github/workflows/publish-nightly-next.yml b/.github/workflows/publish-nightly-next.yml deleted file mode 100644 index 224fb9ae..00000000 --- a/.github/workflows/publish-nightly-next.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Publish nightly (next) - -on: - schedule: - - cron: '50 18 * * *' - workflow_dispatch: - -jobs: - publish: - runs-on: ubuntu-latest - env: - NPM_SECRET: ${{ secrets.NPM_SECRET }} - BRANCH: aiscript-next - TAG: next - - steps: - - name: Checkout - uses: actions/checkout@v4.1.3 - with: - ref: ${{ env.BRANCH }} - - - name: Setup Node.js - uses: actions/setup-node@v4.0.2 - with: - node-version: 20.x - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ~/.npm - key: npm-${{ hashFiles('package-lock.json') }} - restore-keys: npm- - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Set Version - run: | - TIME_STAMP=$( date +'%Y%m%d' ) - VERSION_SUFFIX=-$TAG.$TIME_STAMP - vim package.json '+/"version"' '+s/:\s*".*\zs\ze"/'$VERSION_SUFFIX/ '+wq' - - - name: Check Commits - run: | - echo 'LAST_COMMITS='$( git log --since '24 hours ago' | wc -c ) >> $GITHUB_ENV - - - name: Publish - uses: JS-DevTools/npm-publish@v3 - if: ${{ env.NPM_SECRET != '' && env.LAST_COMMITS != 0 }} - with: - token: ${{ env.NPM_SECRET }} - tag: ${{ env.TAG }} - access: public diff --git a/.github/workflows/publish-nightly-dev.yml b/.github/workflows/publish-nightly.yml similarity index 63% rename from .github/workflows/publish-nightly-dev.yml rename to .github/workflows/publish-nightly.yml index 7402073d..6669ba33 100644 --- a/.github/workflows/publish-nightly-dev.yml +++ b/.github/workflows/publish-nightly.yml @@ -1,4 +1,4 @@ -name: Publish nightly (dev) +name: Publish nightly on: schedule: @@ -8,16 +8,22 @@ on: jobs: publish: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - branch: master + tag: dev + - branch: aiscript-next + tag: next env: NPM_SECRET: ${{ secrets.NPM_SECRET }} - BRANCH: master - TAG: dev steps: - - name: Checkout - uses: actions/checkout@v4.1.3 + - name: Checkout ${{ matrix.branch }} + uses: actions/checkout@v4.1.5 with: - ref: ${{ env.BRANCH }} + ref: ${{ matrix.branch }} - name: Setup Node.js uses: actions/setup-node@v4.0.2 @@ -34,23 +40,23 @@ jobs: - name: Install dependencies run: npm ci - - name: Build - run: npm run build - - name: Set Version run: | + CURRENT_VER=$(npm view 'file:.' version) TIME_STAMP=$( date +'%Y%m%d' ) - VERSION_SUFFIX=-$TAG.$TIME_STAMP - vim package.json '+/"version"' '+s/:\s*".*\zs\ze"/'$VERSION_SUFFIX/ '+wq' + echo 'NEWVERSION='$CURRENT_VER-${{ matrix.tag }}.$TIME_STAMP >> $GITHUB_ENV - name: Check Commits run: | echo 'LAST_COMMITS='$( git log --since '24 hours ago' | wc -c ) >> $GITHUB_ENV + - name: Prepare Publish + run: npm run pre-release + - name: Publish uses: JS-DevTools/npm-publish@v3 if: ${{ env.NPM_SECRET != '' && env.LAST_COMMITS != 0 }} with: token: ${{ env.NPM_SECRET }} - tag: ${{ env.TAG }} + tag: ${{ matrix.tag }} access: public diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f4c95cc0..3e285926 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.5 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4.0.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index ededec23..bc8ae5f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,5 @@ [Read translated version (en)](./translations/en/CHANGELOG.md) -# Next -- 新しいAiScriptパーサーを実装 - - スペースの厳密さが緩和 - - **Breaking Change** 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができる部分以外では文法エラーになります。 -- 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように -- 文法エラーの表示を改善。理由を詳細に表示するように。 -- 複数行のコメントがある時に文法エラーの表示行数がずれる問題を解消しました。 -- 実行時エラーの発生位置が表示されるように。 -- **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 -- **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 -- **Breaking Change** 配列及び関数の引数において、空白区切りが使用できなくなりました。`,`または改行が必要です。 -- **Breaking Change** 関数同士の比較の実装 -- **Breaking Change** `+`や`!`などの演算子の優先順位に変更があります。新しい順序は[syntax.md](docs/syntax.md#%E6%BC%94%E7%AE%97%E5%AD%90)を参照して下さい。 -- **Breaking Change** 組み込み関数`Num:to_hex`は組み込みプロパティ`num#to_hex`に移動しました。 -- **Breaking Change** `arr.sort`を安定ソートに変更 - -# 未リリース分 - # 0.18.0 - `Core:abort`でプログラムを緊急停止できるように - `index_of`の配列版を追加 @@ -143,4 +125,4 @@ - 空の関数を定義できない問題を修正 - 空のスクリプトが許可されていない問題を修正 - ネームスペース付き変数のインクリメント、デクリメントを修正 -- ネームスペース付き変数への代入ができない問題を修正 +- ネームスペース付き変数への代入ができない問題を修正 diff --git a/docs/primitive-props.md b/docs/primitive-props.md index 147f1fbf..cfaa1687 100644 --- a/docs/primitive-props.md +++ b/docs/primitive-props.md @@ -64,6 +64,20 @@ Core:range(0,2).push(4) //[0,1,2,4] ### @(_v_: str).incl(_keyword_: str): bool 文字列中に _keyword_ が含まれていれば`true`、なければ`false`を返します。 +### @(_v_: str).starts_with(_prefix_: str, _start\_index_?: num): bool +文字列が _prefix_ で始まっていれば`true`、そうでなければ`false`を返します。\ +_prefix_ が空文字列の場合は常に`true`を返します。\ +_start\_index_ が指定されている場合、そのインデックスから始めます。\ +_start\_index_ が`v.len`より大きいか`-v.len`より小さい場合は`false`を返します。\ +_start\_index_ が負の場合は末尾から数えます。 + +### @(_v_: str).ends_with(_suffix_: str, _end\_index_?: num): bool +文字列が _suffix_ で終わっていれば`true`、そうでなければ`false`を返します。\ +_suffix_ が空文字列の場合は常に`true`を返します。\ +_end\_index_ が指定されている場合、そのインデックスの直前を末尾とします。(省略時は`v.len`)\ +_end\_index_ が`v.len`より大きいか`-v.len`より小さい場合は`false`を返します。\ +_end\_index_ が負の場合は末尾から数えます。 + ### @(_v_: str).slice(_begin_: num, _end_: num): str 文字列の _begin_ 番目から _end_ 番目の直前までの部分を取得します。 @@ -80,6 +94,16 @@ _fromIndex_が指定されていれば、その位置から検索を開始しま _fromIndex_が負値の時は末尾からの位置(文字列の長さ+_fromIndex_)が使用されます。 該当が無ければ-1を返します。 +### @(_v_: str).pad_start(_width_: num, _pad_?: str): str +文字列の長さがが _width_ になるように、先頭を _pad_ の繰り返しで埋めた新しい文字列を返します。\ +_pad_ を省略した場合、空白`' '`で埋められます。\ +_pad_ が長すぎる場合、_pad_ の末尾が切り捨てられます。 + +### @(_v_: str).pad_end(_width_: num, _pad_?: str): str +文字列の長さがが _width_ になるように、末尾を _pad_ の繰り返しで埋めた新しい文字列を返します。\ +_pad_ を省略した場合、空白`' '`で埋められます。\ +_pad_ が長すぎる場合、_pad_ の末尾が切り捨てられます。 + ### @(_v_: str).trim(): str 文字列の前後の空白を取り除いたものを返します。 @@ -142,11 +166,13 @@ _i_ 番目の文字が存在しない場合は null が返されます。 配列の要素のうち _func_ が true を返すようなもののみを抜き出して返します。 順序は維持されます。 -### @(_v_: arr).reduce(_func_: @(_acm_: value, _item_: value, _index_: num) { value }, _initial_: value): value +### @(_v_: arr).reduce(_func_: Callback, _initial_: value): value +`Callback`: @(_acm_: value, _item_: value, _index_: num): value 配列の各要素に対し _func_ を順番に呼び出します。 各呼び出しでは、前回の結果が第1引数 _acm_ として渡されます。 _initial_ が指定された場合は初回呼び出しの引数が(_initial_, _v_\[0], 0)、 指定されなかった場合は(_v_\[0], _v_\[1], 1)となります。 +配列が空配列であり、かつ _initial_ が指定されていない場合はエラーになります。従って基本的には _initial_ を指定しておくことが推奨されています。 ### @(_v_: arr).find(_func_: @(_item_: value, _index_: num) { bool }): value 配列から _func_ が true を返すような要素を探し、その値を返します。 @@ -187,6 +213,35 @@ _fromIndex_ および _toIndex_ に関する挙動は`arr.slice`に準拠しま `arr.copy`同様シャローコピーであり、配列やオブジェクトの参照は維持されます。 _times_ には0以上の整数値を指定します。それ以外ではエラーになります。 +### @(_v_: arr).splice(_index_: num, _remove_count_?: num, _items_?: arr\): arr\ +**【この操作は配列を書き換えます】** +配列の _index_ から _remove_count_ 個の要素を取り除き、その位置に _items_ の要素を挿入します。 +返り値として、取り除いた要素の配列を返します。\ +_index_ が負の場合は末尾から数えます。\ +_index_ が最後の要素より後の場合は要素を取り除かず、挿入は末尾に追加します。\ +_remove_count_ を省略した場合、末尾まで取り除きます。\ +_items_ を省略した場合、何も挿入しません。 + +### @(_v_: arr).flat(_depth_?: num): arr +配列に含まれる配列を _depth_ で指定した深さの階層まで結合した新しい配列を作成します。 +_depth_ には0以上の整数値を指定します。省略時は1になります。 + +### @(_v_: arr).flat_map(_func_: @(_item_: value, _index_: num) { value }): arr +配列の各要素を _func_ の返り値で置き換えた後、1階層平坦化した新しい配列を作成します。 +_func_ は非同期的に呼び出されます。 + +### @(_v_: arr).insert(_index_: num, _item_: value): null +**【この操作は配列を書き換えます】** +配列の _index_ の位置に _item_ を挿入します。\ +_index_ が負の場合は末尾から数えます。\ +_index_ が最後の要素より後の場合は末尾に追加します。 + +### @(_v_: arr).remove(_index_: num): value | null +**【この操作は配列を書き換えます】** +配列から _index_ の位置の要素を取り除き、その要素を返します。\ +_index_ が負の場合は末尾から数えます。\ +_index_ が最後の要素より後の場合は取り除かず、`null`を返します。 + ### @(_v_: arr).every(_func_: @(_item_: value, _index_: num) { bool }): bool 配列の全ての要素に対して _func_ が true を返す時のみ true 返します。空配列には常に true を返します。 diff --git a/docs/std.md b/docs/std.md index 63ba904e..65516602 100644 --- a/docs/std.md +++ b/docs/std.md @@ -113,27 +113,44 @@ _time_offset_ を渡していない場合はローカルのものを参照しま 型: `str` 改行コード(LF)です。 -### #Str:lt(a: str, b: str): num +### @Str:lt(a: str, b: str): num a < b ならば -1、a == b ならば 0、a > b ならば 1 を返します。 arr.sortの比較関数として使用できます。 -### #Str:gt(a: str, b: str): num +### @Str:gt(a: str, b: str): num a > b ならば -1、a == b ならば 0、a < b ならば 1 を返します。 arr.sortの比較関数として使用できます。 -### #Str:from_codepoint(codepoint: num): str +### @Str:from_codepoint(codepoint: num): str Unicodeのコードポイントから文字を生成します。 _codepoint_ は 0 以上、10FFFF16 以下である必要があります。 -### #Str:from_unicode_codepoints(_codePoints_: `arr`): str +### @Str:from_unicode_codepoints(_codePoints_: `arr`): str Unicodeのコードポイント列を表す数値の配列から文字を生成します。 _codePoints_の各要素は 0 以上、10FFFF16 以下である必要があります。 -### #Str:from_utf8_bytes(_bytes_: `arr`): str +### @Str:from_utf8_bytes(_bytes_: `arr`): str UTF-8のバイト列を表す数値の配列から文字を生成します。 _bytes_の各要素は 0 以上、255 以下である必要があります。 +## :: Uri +### @Uri:encode_full(uri: str): str +uri をURIとしてエンコードした文字列を返します。以下の文字はエンコードされません。 +`A-Z a-z 0-9 - _ . ! ~ * ' ( ) ; , / ? : @ & = + $ #` + +### @Uri:encode_component(text: str): str +text をURI構成要素としてエンコードした文字列を返します。以下の文字はエンコードされません。 +`A-Z a-z 0-9 - _ . ! ~ * ' ( )` + +### @Uri:decode_full(encoded_uri: str): str +encoded_uri をエンコードされたURIとしてデコードした文字列を返します。 +以下の文字に対応するエスケープシーケンスはデコードされません。 +`; , / ? : @ & = + $ #` + +### @Uri:decode_component(encoded_text: str): str +encoded_text をエンコードされたURI構成要素としてデコードした文字列を返します。 + ## :: Arr ### @Arr:create(_length_: num, _initial_?: value): arr 長さが`length`の配列を作成します。 diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index cf8ea2b8..caf7f7e2 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -78,6 +78,7 @@ type And = NodeBase & { type: 'and'; left: Expression; right: Expression; + operatorLoc: Loc; }; // @public (undocumented) @@ -479,6 +480,7 @@ type Or = NodeBase & { type: 'or'; left: Expression; right: Expression; + operatorLoc: Loc; }; // @public (undocumented) diff --git a/package-lock.json b/package-lock.json index 5d153106..15b55102 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@syuilo/aiscript", - "version": "0.18.0", + "version": "0.19.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@syuilo/aiscript", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "dependencies": { "seedrandom": "3.0.5", @@ -27,6 +27,7 @@ "eslint-plugin-import": "2.29.1", "jest": "29.7.0", "peggy": "4.0.2", + "semver": "7.6.2", "ts-jest": "29.1.2", "ts-jest-resolver": "2.0.1", "ts-node": "10.9.2", @@ -1504,6 +1505,21 @@ "node": "*" } }, + "node_modules/@microsoft/api-extractor/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", @@ -1619,6 +1635,21 @@ } } }, + "node_modules/@rushstack/node-core-library/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rushstack/rig-package": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz", @@ -7178,13 +7209,10 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -9360,6 +9388,15 @@ "requires": { "brace-expansion": "^1.1.7" } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } } } }, @@ -9462,6 +9499,17 @@ "resolve": "~1.22.1", "semver": "~7.5.4", "z-schema": "~5.0.2" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "@rushstack/rig-package": { @@ -13540,13 +13588,10 @@ "version": "3.0.5" }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true }, "shebang-command": { "version": "2.0.0", diff --git a/package.json b/package.json index 3ebf077e..3dcc030f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,9 @@ "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "jest": "jest --coverage --detectOpenHandles", "tsd": "tsd", - "test": "npm run jest" + "test": "npm run jest", + "pre-release": "node scripts/pre-release.mjs && npm run build", + "prepublishOnly": "node scripts/check-release.mjs" }, "devDependencies": { "@microsoft/api-extractor": "7.42.3", @@ -43,6 +45,7 @@ "eslint": "8.57.0", "eslint-plugin-import": "2.29.1", "jest": "29.7.0", + "semver": "7.6.2", "ts-jest": "29.1.2", "ts-jest-resolver": "2.0.1", "ts-node": "10.9.2", diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs new file mode 100644 index 00000000..4d218aeb --- /dev/null +++ b/scripts/check-release.mjs @@ -0,0 +1,8 @@ +import { readdir } from 'node:fs/promises'; + +await readdir('./unreleased') + .then(pathes => { + if (pathes.length > 1 || (pathes.length === 1 && pathes[0] !== '.gitkeep')) throw new Error('Run "npm run pre-release" before publish.') + }, err => { + if (err.code !== 'ENOENT') throw err; + }); diff --git a/scripts/gen-pkg-ts.mjs b/scripts/gen-pkg-ts.mjs index f6d757a3..2e22d069 100644 --- a/scripts/gen-pkg-ts.mjs +++ b/scripts/gen-pkg-ts.mjs @@ -1,6 +1,6 @@ -import { writeFile } from 'node:fs/promises'; -import pkg from '../package.json' assert { type: 'json' }; +import { readFile, writeFile } from 'node:fs/promises'; +const pkg = JSON.parse((await readFile('./package.json', 'utf8'))); await writeFile('./src/pkg.ts', `/* This file is automatically generated by scripts/gen-pkg-ts.js. diff --git a/scripts/pre-release.mjs b/scripts/pre-release.mjs new file mode 100644 index 00000000..1dfd7dfb --- /dev/null +++ b/scripts/pre-release.mjs @@ -0,0 +1,101 @@ +import { readFile, readdir, writeFile, mkdir, rm } from 'node:fs/promises'; +import { env } from 'node:process'; +import { promisify } from 'node:util'; +import child_process from 'node:child_process'; +import semverValid from 'semver/functions/valid.js'; + + +const exec = promisify(child_process.exec); +const FILES = { + chlog: './CHANGELOG.md', + chlogs: './unreleased', + pkgjson: './package.json', +}; +const enc = { encoding: env.ENCODING ?? 'utf8' }; +const pkgjson = JSON.parse(await readFile(FILES.pkgjson, enc)); +const newver = (() => { + const newverCandidates = [ + [env.NEWVERSION, 'Environment variable NEWVERSION'], + [pkgjson.version, "Package.json's version field"], + ]; + for (const [ver, name] of newverCandidates) { + if (ver) { + if (semverValid(ver)) return ver; + else throw new Error(`${name} is set to "${ver}"; it is not valid`); + } + } + throw new Error('No effective version setting detected.'); +})(); +const actions = {}; + +/* + * Update package.json's version field + */ +actions.updatePackageJson = { + async read() { + return JSON.stringify( + { ...pkgjson, version: newver }, + null, '\t' + ); + }, + async write(json) { + return writeFile(FILES.pkgjson, json); + }, +}; + +/* + * Collect changelogs + */ +actions.collectChangeLogs = { + async read() { + const getNewLog = async () => { + const pathes = (await readdir(FILES.chlogs)).map(path => `${FILES.chlogs}/${path}`); + const pathesLastUpdate = await Promise.all( + pathes.map(async (path) => { + const gittime = Number((await exec( + `git log -1 --pretty="format:%ct" "${path}"` + )).stdout); + if (gittime) return { path, lastUpdate: gittime }; + else { + console.log(`Warning: git timestamp of "${path}" was not detected`); + return { path, lastUpdate: Infinity } + } + }) + ); + pathesLastUpdate.sort((a, b) => a.lastUpdate - b.lastUpdate); + const logPromises = pathesLastUpdate.map(({ path }) => readFile(path, enc)); + const logs = await Promise.all(logPromises); + return logs.map(v => v.trim()).join('\n'); + }; + const getOldLog = async () => { + const log = await readFile(FILES.chlog, enc); + const idx = log.indexOf('#'); + return [ + log.slice(0, idx), + log.slice(idx), + ]; + }; + const [newLog, [logHead, oldLog]] = await Promise.all([ getNewLog(), getOldLog() ]); + return `${logHead}# ${newver}\n${newLog}\n\n${oldLog}`; + + }, + async write(logs) { + return Promise.all([ + writeFile(FILES.chlog, logs), + rm(FILES.chlogs, { + recursive: true, + force: true, + }).then(() => + mkdir(FILES.chlogs) + ).then(() => + writeFile(`${FILES.chlogs}/.gitkeep`, '')) + ]); + }, +}; + +// read all before writing +const reads = await Promise.all(Object.entries(actions).map(async ([name, { read }]) => [name, await read().catch(err => { throw new Error(`in actions.${name}.read: ${err}`) })])); + +// write after reading all +await Promise.all(reads.map(([name, read]) => actions[name].write(read).catch(err => { throw new Error(`in actions.${name}.write: ${err}`) }))); + diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index bc77a996..3401b6d5 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -234,7 +234,7 @@ export class Interpreter { for (let i = 0; i < (fn.args ?? []).length; i++) { _args.set(fn.args![i]!, { isMutable: true, - value: args[i]!, + value: args[i] ?? NULL, }); } const fnScope = fn.scope!.createChildScope(_args); diff --git a/src/interpreter/lib/std.ts b/src/interpreter/lib/std.ts index a34e62e6..f3fbbc86 100644 --- a/src/interpreter/lib/std.ts +++ b/src/interpreter/lib/std.ts @@ -183,37 +183,37 @@ export const std: Record = { 'Date:year': FN_NATIVE(([v]) => { if (v) { assertNumber(v); } - return NUM(new Date(v?.value || Date.now()).getFullYear()); + return NUM(new Date(v?.value ?? Date.now()).getFullYear()); }), 'Date:month': FN_NATIVE(([v]) => { if (v) { assertNumber(v); } - return NUM(new Date(v?.value || Date.now()).getMonth() + 1); + return NUM(new Date(v?.value ?? Date.now()).getMonth() + 1); }), 'Date:day': FN_NATIVE(([v]) => { if (v) { assertNumber(v); } - return NUM(new Date(v?.value || Date.now()).getDate()); + return NUM(new Date(v?.value ?? Date.now()).getDate()); }), 'Date:hour': FN_NATIVE(([v]) => { if (v) { assertNumber(v); } - return NUM(new Date(v?.value || Date.now()).getHours()); + return NUM(new Date(v?.value ?? Date.now()).getHours()); }), 'Date:minute': FN_NATIVE(([v]) => { if (v) { assertNumber(v); } - return NUM(new Date(v?.value || Date.now()).getMinutes()); + return NUM(new Date(v?.value ?? Date.now()).getMinutes()); }), 'Date:second': FN_NATIVE(([v]) => { if (v) { assertNumber(v); } - return NUM(new Date(v?.value || Date.now()).getSeconds()); + return NUM(new Date(v?.value ?? Date.now()).getSeconds()); }), 'Date:millisecond': FN_NATIVE(([v]) => { if (v) { assertNumber(v); } - return NUM(new Date(v?.value || Date.now()).getMilliseconds()); + return NUM(new Date(v?.value ?? Date.now()).getMilliseconds()); }), 'Date:parse': FN_NATIVE(([v]) => { @@ -528,6 +528,28 @@ export const std: Record = { }), //#endregion + //#region Uri + 'Uri:encode_full': FN_NATIVE(([v]) => { + assertString(v); + return STR(encodeURI(v.value)); + }), + + 'Uri:encode_component': FN_NATIVE(([v]) => { + assertString(v); + return STR(encodeURIComponent(v.value)); + }), + + 'Uri:decode_full': FN_NATIVE(([v]) => { + assertString(v); + return STR(decodeURI(v.value)); + }), + + 'Uri:decode_component': FN_NATIVE(([v]) => { + assertString(v); + return STR(decodeURIComponent(v.value)); + }), + //#endregion + //#region Arr 'Arr:create': FN_NATIVE(([length, initial]) => { assertNumber(length); diff --git a/src/interpreter/primitive-props.ts b/src/interpreter/primitive-props.ts index 13827fc2..90cc65d4 100644 --- a/src/interpreter/primitive-props.ts +++ b/src/interpreter/primitive-props.ts @@ -2,7 +2,7 @@ import { substring, length, indexOf, toArray } from 'stringz'; import { AiScriptRuntimeError } from '../error.js'; import { textEncoder } from '../const.js'; -import { assertArray, assertBoolean, assertFunction, assertNumber, assertString, expectAny, eq } from './util.js'; +import { assertArray, assertBoolean, assertFunction, assertNumber, assertString, expectAny, eq, isArray } from './util.js'; import { ARR, FALSE, FN_NATIVE, NULL, NUM, STR, TRUE } from './value.js'; import type { Value, VArr, VFn, VNum, VStr, VError } from './value.js'; @@ -123,6 +123,51 @@ const PRIMITIVE_PROPS: { const res = target.value.codePointAt(i.value) ?? target.value.charCodeAt(i.value); return Number.isNaN(res) ? NULL : NUM(res); }), + + starts_with: (target: VStr): VFn => FN_NATIVE(async ([prefix, start_index], _opts) => { + assertString(prefix); + if (!prefix.value) { + return TRUE; + } + + if (start_index) assertNumber(start_index); + const raw_index = start_index?.value ?? 0; + if (raw_index < -target.value.length || raw_index > target.value.length) { + return FALSE; + } + const index = (raw_index >= 0) ? raw_index : target.value.length + raw_index; + return target.value.startsWith(prefix.value, index) ? TRUE : FALSE; + }), + + ends_with: (target: VStr): VFn => FN_NATIVE(async ([suffix, end_index], _opts) => { + assertString(suffix); + if (!suffix.value) { + return TRUE; + } + + if (end_index) assertNumber(end_index); + const raw_index = end_index?.value ?? target.value.length; + if (raw_index < -target.value.length || raw_index > target.value.length) { + return FALSE; + } + const index = (raw_index >= 0) ? raw_index : target.value.length + raw_index; + + return target.value.endsWith(suffix.value, index) ? TRUE : FALSE; + }), + + pad_start: (target: VStr): VFn => FN_NATIVE(([width, pad], _) => { + assertNumber(width); + const s = (pad) ? (assertString(pad), pad.value) : ' '; + + return STR(target.value.padStart(width.value, s)); + }), + + pad_end: (target: VStr): VFn => FN_NATIVE(([width, pad], _) => { + assertNumber(width); + const s = (pad) ? (assertString(pad), pad.value) : ' '; + + return STR(target.value.padEnd(width.value, s)); + }), }, arr: { @@ -187,6 +232,7 @@ const PRIMITIVE_PROPS: { reduce: (target: VArr): VFn => FN_NATIVE(async ([fn, initialValue], opts) => { assertFunction(fn); const withInitialValue = initialValue != null; + if (!withInitialValue && (target.value.length === 0)) throw new AiScriptRuntimeError('Reduce of empty array without initial value'); let accumulator = withInitialValue ? initialValue : target.value[0]!; for (let i = withInitialValue ? 0 : 1; i < target.value.length; i++) { const item = target.value[i]!; @@ -235,8 +281,9 @@ const PRIMITIVE_PROPS: { const mergeSort = async (arr: Value[], comp: VFn): Promise => { if (arr.length <= 1) return arr; const mid = Math.floor(arr.length / 2); - const left = await mergeSort(arr.slice(0, mid), comp); - const right = await mergeSort(arr.slice(mid), comp); + const left_promise = mergeSort(arr.slice(0, mid), comp); + const right_promise = mergeSort(arr.slice(mid), comp); + const [left, right] = await Promise.all([left_promise, right_promise]); return merge(left, right, comp); }; const merge = async (left: Value[], right: Value[], comp: VFn): Promise => { @@ -283,6 +330,55 @@ const PRIMITIVE_PROPS: { throw e; } }), + + splice: (target: VArr): VFn => FN_NATIVE(async ([idx, rc, vs], opts) => { + assertNumber(idx); + const index = (idx.value < -target.value.length) ? 0 + : (idx.value < 0) ? target.value.length + idx.value + : (idx.value >= target.value.length) ? target.value.length + : idx.value; + + const remove_count = (rc != null) ? (assertNumber(rc), rc.value) + : target.value.length - index; + + const items = (vs != null) ? (assertArray(vs), vs.value) : []; + + const result = target.value.splice(index, remove_count, ...items); + return ARR(result); + }), + + flat: (target: VArr): VFn => FN_NATIVE(async ([depth], opts) => { + depth = depth ?? NUM(1); + assertNumber(depth); + if (!Number.isInteger(depth.value)) throw new AiScriptRuntimeError('arr.flat expected integer, got non-integer'); + if (depth.value < 0) throw new AiScriptRuntimeError('arr.flat expected non-negative number, got negative'); + const flat = (arr: Value[], depth: number, result: Value[]) => { + if (depth === 0) { + result.push(...arr); + return; + } + for (const v of arr) { + if (isArray(v)) { + flat(v.value, depth - 1, result); + } else { + result.push(v); + } + } + }; + const result: Value[] = []; + flat(target.value, depth.value, result); + return ARR(result); + }), + + flat_map: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => { + assertFunction(fn); + const vals = target.value.map(async (item, i) => { + const result = await opts.call(fn, [item, NUM(i)]); + return isArray(result) ? result.value : result; + }); + const mapped_vals = await Promise.all(vals); + return ARR(mapped_vals.flat()); + }), every: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => { assertFunction(fn); @@ -305,6 +401,23 @@ const PRIMITIVE_PROPS: { } return FALSE; }), + + insert: (target: VArr): VFn => FN_NATIVE(async ([index, item], opts) => { + assertNumber(index); + expectAny(item); + + target.value.splice(index.value, 0, item); + + return NULL; + }), + + remove: (target: VArr): VFn => FN_NATIVE(async ([index], opts) => { + assertNumber(index); + + const removed = target.value.splice(index.value, 1); + + return removed[0] ?? NULL; + }), }, error: { diff --git a/src/node.ts b/src/node.ts index caa33196..03d737d2 100644 --- a/src/node.ts +++ b/src/node.ts @@ -152,12 +152,14 @@ export type And = NodeBase & { type: 'and'; left: Expression; right: Expression; + operatorLoc: Loc; } export type Or = NodeBase & { type: 'or'; left: Expression; right: Expression; + operatorLoc: Loc; } export type If = NodeBase & { diff --git a/test/index.ts b/test/index.ts index f12de651..621533fe 100644 --- a/test/index.ts +++ b/test/index.ts @@ -4,6 +4,7 @@ */ import * as assert from 'assert'; +import { expect, test } from '@jest/globals'; import { Parser, Interpreter, utils, errors, Ast } from '../src'; import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; import { AiScriptSyntaxError } from '../src/error'; @@ -1691,6 +1692,16 @@ describe('Function call', () => { } assert.fail(); }); + + test.concurrent('omitted args', async () => { + const res = await exe(` + @f(x, y) { + [x, y] + } + <: f(1) + `); + eq(res, ARR([NUM(1), NULL])); + }); }); describe('Return', () => { @@ -2758,6 +2769,20 @@ describe('Location', () => { if (!node.loc) assert.fail(); assert.deepEqual(node.loc, { line: 2, column: 4 }); }); + test.concurrent('comment', async () => { + let node: Ast.Node; + const parser = new Parser(); + const nodes = parser.parse(` + /* + */ + // hoge + @f(a) { a } + `); + assert.equal(nodes.length, 1); + node = nodes[0]; + if (!node.loc) assert.fail(); + assert.deepEqual(node.loc, { line: 5, column: 3 }); + }); }); describe('Variable declaration', () => { @@ -2986,6 +3011,122 @@ describe('primitive props', () => { ARR([NUM(97), NUM(98), NUM(99), NUM(240), NUM(169), NUM(184), NUM(189), NUM(240), NUM(159), NUM(145), NUM(137), NUM(240), NUM(159), NUM(143), NUM(191), NUM(240), NUM(159), NUM(145), NUM(168), NUM(226), NUM(128), NUM(141), NUM(240), NUM(159), NUM(145), NUM(166), NUM(100), NUM(101), NUM(102)]) ); }); + + test.concurrent('starts_with (no index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.starts_with(""), str.starts_with("hello"), + str.starts_with("he"), str.starts_with("ell"), + empty.starts_with(""), empty.starts_with("he"), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, FALSE, + TRUE, FALSE, + ])); + }); + + test.concurrent('starts_with (with index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.starts_with("", 4), str.starts_with("he", 0), + str.starts_with("ll", 2), str.starts_with("lo", 3), + str.starts_with("lo", -2), str.starts_with("hel", -5), + str.starts_with("he", 2), str.starts_with("loa", 3), + str.starts_with("lo", -6), str.starts_with("", -7), + str.starts_with("lo", 6), str.starts_with("", 7), + empty.starts_with("", 2), empty.starts_with("ll", 2), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, TRUE, + TRUE, TRUE, + FALSE, FALSE, + FALSE, TRUE, + FALSE, TRUE, + TRUE, FALSE, + ])); + }); + + test.concurrent('ends_with (no index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.ends_with(""), str.ends_with("hello"), + str.ends_with("lo"), str.ends_with("ell"), + empty.ends_with(""), empty.ends_with("he"), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, FALSE, + TRUE, FALSE, + ])); + }); + + test.concurrent('ends_with (with index)', async () => { + const res = await exe(` + let str = "hello" + let empty = "" + <: [ + str.ends_with("", 3), str.ends_with("lo", 5), + str.ends_with("ll", 4), str.ends_with("he", 2), + str.ends_with("ll", -1), str.ends_with("he", -3), + str.ends_with("he", 5), str.ends_with("lo", 3), + str.ends_with("lo", -6), str.ends_with("", -7), + str.ends_with("lo", 6), str.ends_with("", 7), + empty.ends_with("", 2), empty.ends_with("ll", 2), + ] + `); + eq(res, ARR([ + TRUE, TRUE, + TRUE, TRUE, + TRUE, TRUE, + FALSE, FALSE, + FALSE, TRUE, + FALSE, TRUE, + TRUE, FALSE, + ])); + }); + + test.concurrent("pad_start", async () => { + const res = await exe(` + let str = "abc" + <: [ + str.pad_start(0), str.pad_start(1), str.pad_start(2), str.pad_start(3), str.pad_start(4), str.pad_start(5), + str.pad_start(0, "0"), str.pad_start(1, "0"), str.pad_start(2, "0"), str.pad_start(3, "0"), str.pad_start(4, "0"), str.pad_start(5, "0"), + str.pad_start(0, "01"), str.pad_start(1, "01"), str.pad_start(2, "01"), str.pad_start(3, "01"), str.pad_start(4, "01"), str.pad_start(5, "01"), + ] + `); + eq(res, ARR([ + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR(" abc"), STR(" abc"), + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("0abc"), STR("00abc"), + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("0abc"), STR("01abc"), + ])); + }); + + test.concurrent("pad_end", async () => { + const res = await exe(` + let str = "abc" + <: [ + str.pad_end(0), str.pad_end(1), str.pad_end(2), str.pad_end(3), str.pad_end(4), str.pad_end(5), + str.pad_end(0, "0"), str.pad_end(1, "0"), str.pad_end(2, "0"), str.pad_end(3, "0"), str.pad_end(4, "0"), str.pad_end(5, "0"), + str.pad_end(0, "01"), str.pad_end(1, "01"), str.pad_end(2, "01"), str.pad_end(3, "01"), str.pad_end(4, "01"), str.pad_end(5, "01"), + ] + `); + eq(res, ARR([ + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("abc "), STR("abc "), + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("abc0"), STR("abc00"), + STR("abc"), STR("abc"), STR("abc"), STR("abc"), STR("abc0"), STR("abc01"), + ])); + }); }); describe('arr', () => { @@ -3113,6 +3254,13 @@ describe('primitive props', () => { eq(res, NUM(20)); }); + test.concurrent('reduce of empty array without initial value', async () => { + await expect(exe(` + let arr = [1, 2, 3, 4] + <: [].reduce(@(){}) + `)).rejects.toThrow('Reduce of empty array without initial value'); + }); + test.concurrent('find', async () => { const res = await exe(` let arr = ["abc", "def", "ghi"] @@ -3267,7 +3415,98 @@ describe('primitive props', () => { ARR([]), ])); }); + + test.concurrent('splice (full)', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(1, 2, [10]) + <: [arr1, arr2] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(10), NUM(3)]), + ARR([NUM(1), NUM(2)]), + ])); + }); + + test.concurrent('splice (negative-index)', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(-1, 0, [10, 20]) + <: [arr1, arr2] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2), NUM(10), NUM(20), NUM(3)]), + ARR([]), + ])); + }); + + test.concurrent('splice (larger-index)', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(4, 100, [10, 20]) + <: [arr1, arr2] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2), NUM(3), NUM(10), NUM(20)]), + ARR([]), + ])); + }); + + test.concurrent('splice (single argument)', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3] + let arr2 = arr1.splice(1) + <: [arr1, arr2] + `); + eq(res, ARR([ + ARR([NUM(0)]), + ARR([NUM(1), NUM(2), NUM(3)]), + ])); + }); + test.concurrent('flat', async () => { + const res = await exe(` + var arr1 = [0, [1], [2, 3], [4, [5, 6]]] + let arr2 = arr1.flat() + let arr3 = arr1.flat(2) + <: [arr1, arr2, arr3] + `); + eq(res, ARR([ + ARR([ + NUM(0), ARR([NUM(1)]), ARR([NUM(2), NUM(3)]), + ARR([NUM(4), ARR([NUM(5), NUM(6)])]) + ]), // target not changed + ARR([ + NUM(0), NUM(1), NUM(2), NUM(3), + NUM(4), ARR([NUM(5), NUM(6)]), + ]), + ARR([ + NUM(0), NUM(1), NUM(2), NUM(3), + NUM(4), NUM(5), NUM(6), + ]), + ])); + }); + + test.concurrent('flat_map', async () => { + const res = await exe(` + let arr1 = [0, 1, 2] + let arr2 = ["a", "b"] + let arr3 = arr1.flat_map(@(x){ arr2.map(@(y){ [x, y] }) }) + <: [arr1, arr3] + `); + eq(res, ARR([ + ARR([NUM(0), NUM(1), NUM(2)]), // target not changed + ARR([ + ARR([NUM(0), STR("a")]), + ARR([NUM(0), STR("b")]), + ARR([NUM(1), STR("a")]), + ARR([NUM(1), STR("b")]), + ARR([NUM(2), STR("a")]), + ARR([NUM(2), STR("b")]), + ]), + ])); + }); + test.concurrent('every', async () => { const res = await exe(` let arr1 = [0, 1, 2, 3] @@ -3297,6 +3536,44 @@ describe('primitive props', () => { FALSE, ])); }); + + test.concurrent('insert', async () => { + const res = await exe(` + let arr1 = [0, 1, 2] + let res = [] + res.push(arr1.insert(3, 10)) // [0, 1, 2, 10] + res.push(arr1.insert(2, 20)) // [0, 1, 20, 2, 10] + res.push(arr1.insert(0, 30)) // [30, 0, 1, 20, 2, 10] + res.push(arr1.insert(-1, 40)) // [30, 0, 1, 20, 2, 40, 10] + res.push(arr1.insert(-4, 50)) // [30, 0, 1, 50, 20, 2, 40, 10] + res.push(arr1.insert(100, 60)) // [30, 0, 1, 50, 20, 2, 40, 10, 60] + res.push(arr1) + <: res + `); + eq(res, ARR([ + NULL, NULL, NULL, NULL, NULL, NULL, + ARR([NUM(30), NUM(0), NUM(1), NUM(50), NUM(20), NUM(2), NUM(40), NUM(10), NUM(60)]) + ])); + }); + + test.concurrent('remove', async () => { + const res = await exe(` + let arr1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + let res = [] + res.push(arr1.remove(9)) // 9 [0, 1, 2, 3, 4, 5, 6, 7, 8] + res.push(arr1.remove(3)) // 3 [0, 1, 2, 4, 5, 6, 7, 8] + res.push(arr1.remove(0)) // 0 [1, 2, 4, 5, 6, 7, 8] + res.push(arr1.remove(-1)) // 8 [1, 2, 4, 5, 6, 7] + res.push(arr1.remove(-5)) // 2 [1, 4, 5, 6, 7] + res.push(arr1.remove(100)) // null [1, 4, 5, 6, 7] + res.push(arr1) + <: res + `); + eq(res, ARR([ + NUM(9), NUM(3), NUM(0), NUM(8), NUM(2), NULL, + ARR([NUM(1), NUM(4), NUM(5), NUM(6), NUM(7)]) + ])); + }); }); }); @@ -3425,18 +3702,32 @@ describe('std', () => { }); test.concurrent('gen_rng', async () => { + // 2つのシード値から1~maxの乱数をn回生成して一致率を見る const res = await exe(` - @test(seed) { - let random = Math:gen_rng(seed) - return random(0, 100) + @test(seed1, seed2) { + let n = 100 + let max = 100000 + let threshold = 0.05 + let random1 = Math:gen_rng(seed1) + let random2 = Math:gen_rng(seed2) + var same = 0 + for n { + if random1(1, max) == random2(1, max) { + same += 1 + } + } + let rate = same / n + if seed1 == seed2 { rate == 1 } + else { rate < threshold } } let seed1 = \`{Util:uuid()}\` let seed2 = \`{Date:year()}\` - let test1 = if (test(seed1) == test(seed1)) {true} else {false} - let test2 = if (test(seed1) == test(seed2)) {true} else {false} - <: [test1, test2] + <: [ + test(seed1, seed1) + test(seed1, seed2) + ] `) - eq(res, ARR([BOOL(true), BOOL(false)])); + eq(res, ARR([BOOL(true), BOOL(true)])); }); }); @@ -3525,6 +3816,36 @@ describe('std', () => { }); }); + describe('Uri', () => { + test.concurrent('encode_full', async () => { + const res = await exe(` + <: Uri:encode_full("https://example.com/?q=あいちゃん") + `); + eq(res, STR('https://example.com/?q=%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93')); + }); + + test.concurrent('encode_component', async () => { + const res = await exe(` + <: Uri:encode_component("https://example.com/?q=あいちゃん") + `); + eq(res, STR('https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93')); + }); + + test.concurrent('decode_full', async () => { + const res = await exe(` + <: Uri:decode_full("https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93") + `); + eq(res, STR('https%3A%2F%2Fexample.com%2F%3Fq%3Dあいちゃん')); + }); + + test.concurrent('decode_component', async () => { + const res = await exe(` + <: Uri:decode_component("https%3A%2F%2Fexample.com%2F%3Fq%3D%E3%81%82%E3%81%84%E3%81%A1%E3%82%83%E3%82%93") + `); + eq(res, STR('https://example.com/?q=あいちゃん')); + }); + }); + describe('Error', () => { test.concurrent('create', async () => { eq( @@ -3578,6 +3899,55 @@ describe('std', () => { }); describe('Date', () => { + test.concurrent('year', async () => { + const res = await exe(` + <: [Date:year(0), Date:year(1714946889010)] + `); + eq(res.value, [NUM(1970), NUM(2024)]); + }); + + test.concurrent('month', async () => { + const res = await exe(` + <: [Date:month(0), Date:month(1714946889010)] + `); + eq(res.value, [NUM(1), NUM(5)]); + }); + + test.concurrent('day', async () => { + const res = await exe(` + <: [Date:day(0), Date:day(1714946889010)] + `); + eq(res.value, [NUM(1), NUM(6)]); + }); + + test.concurrent('hour', async () => { + const res = await exe(` + <: [Date:hour(0), Date:hour(1714946889010)] + `); + eq(res.value, [NUM(0), NUM(7)]); + }); + + test.concurrent('minute', async () => { + const res = await exe(` + <: [Date:minute(0), Date:minute(1714946889010)] + `); + eq(res.value, [NUM(0), NUM(8)]); + }); + + test.concurrent('second', async () => { + const res = await exe(` + <: [Date:second(0), Date:second(1714946889010)] + `); + eq(res.value, [NUM(0), NUM(9)]); + }); + + test.concurrent('millisecond', async () => { + const res = await exe(` + <: [Date:millisecond(0), Date:millisecond(1714946889010)] + `); + eq(res.value, [NUM(0), NUM(10)]); + }); + test.concurrent('to_iso_str', async () => { const res = await exe(` let d1 = Date:parse("2024-04-12T01:47:46.021+09:00") diff --git a/translations/en/docs/std.md b/translations/en/docs/std.md index 67d36237..32098a9e 100644 --- a/translations/en/docs/std.md +++ b/translations/en/docs/std.md @@ -113,19 +113,19 @@ Generates a numeric value from a hexadecimal string. Type: `str`. Newline code (LF). -### #Str:lt(a: str, b: str): num +### @Str:lt(a: str, b: str): num Returns -1 if a < b, 0 if a == b, or 1 if a > b. Using this as a comparison function for `arr.sort`, the array is sorted in ascending lexicographic order. -### #Str:gt(a: str, b: str): num +### @Str:gt(a: str, b: str): num Returns -1 if a > b, 0 if a == b, or 1 if a < b. Using this as the comparison function for `arr.sort`, the array is sorted in descending lexicographic order. -### #Str:from_codepoint(codepoint: num): str +### @Str:from_codepoint(codepoint: num): str Generates character from unicode code point. _codepoint_ must be greater than or equal to 0 and less than or equal to 10FFFFFF16. Multiple arguments are not supported yet. diff --git a/unreleased/.gitkeep b/unreleased/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/unreleased/next-past.md b/unreleased/next-past.md new file mode 100644 index 00000000..6f2fd89e --- /dev/null +++ b/unreleased/next-past.md @@ -0,0 +1,14 @@ +- 新しいAiScriptパーサーを実装 + - スペースの厳密さが緩和 + - **Breaking Change** 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができる部分以外では文法エラーになります。 +- 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように +- 文法エラーの表示を改善。理由を詳細に表示するように。 +- 複数行のコメントがある時に文法エラーの表示行数がずれる問題を解消しました。 +- 実行時エラーの発生位置が表示されるように。 +- **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 +- **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 +- **Breaking Change** 配列及び関数の引数において、空白区切りが使用できなくなりました。`,`または改行が必要です。 +- **Breaking Change** 関数同士の比較の実装 +- **Breaking Change** `+`や`!`などの演算子の優先順位に変更があります。新しい順序は[syntax.md](docs/syntax.md#%E6%BC%94%E7%AE%97%E5%AD%90)を参照して下さい。 +- **Breaking Change** 組み込み関数`Num:to_hex`は組み込みプロパティ`num#to_hex`に移動しました。 +- **Breaking Change** `arr.sort`を安定ソートに変更 diff --git a/unreleased/past.md b/unreleased/past.md new file mode 100644 index 00000000..52191011 --- /dev/null +++ b/unreleased/past.md @@ -0,0 +1,10 @@ +- `Date:year`系の関数に0を渡すと現在時刻になる問題を修正 +- シンタックスエラーなどの位置情報を修正 +- `arr.reduce`が空配列に対して初期値なしで呼び出された時、正式にエラーを出すよう +- `str.pad_start`,`str.pad_end`を追加 +- `arr.insert`,`arr.remove`を追加 +- `arr.sort`の処理を非同期的にして高速化 +- `arr.flat`,`arr.flat_map`を追加 +- `Uri:encode_full`, `Uri:encode_component`, `Uri:decode_full`, `Uri:decode_component`を追加 +- `str.starts_with`,`str.ends_with`を追加 +- `arr.splice`を追加