├── .editorconfig ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── api.yml │ ├── gh-pages.yml │ ├── lint.yml │ ├── publish-nightly.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── aiscript.png ├── api-extractor.json ├── codecov.yml ├── docs ├── README.md └── parser │ ├── overview.md │ ├── scanner.md │ └── token-streams.md ├── eslint.config.mjs ├── etc └── aiscript.api.md ├── package-lock.json ├── package.json ├── playground ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── public │ └── .gitkeep ├── src │ ├── App.vue │ ├── Settings.vue │ ├── assets │ │ └── .gitkeep │ └── main.js └── vite.config.js ├── scripts ├── check-release.mjs ├── gen-pkg-ts.mjs ├── parse.mjs ├── pre-release.mjs ├── repl.mjs └── start.mjs ├── src ├── const.ts ├── constants.ts ├── error.ts ├── index.ts ├── interpreter │ ├── control.ts │ ├── index.ts │ ├── lib │ │ └── std.ts │ ├── primitive-props.ts │ ├── reference.ts │ ├── scope.ts │ ├── util.ts │ ├── value.ts │ └── variable.ts ├── node.ts ├── parser │ ├── index.ts │ ├── plugins │ │ ├── validate-jump-statements.ts │ │ ├── validate-keyword.ts │ │ └── validate-type.ts │ ├── scanner.ts │ ├── streams │ │ ├── char-stream.ts │ │ └── token-stream.ts │ ├── syntaxes │ │ ├── common.ts │ │ ├── expressions.ts │ │ ├── statements.ts │ │ ├── toplevel.ts │ │ └── types.ts │ ├── token.ts │ ├── utils.ts │ └── visit.ts ├── type.ts └── utils │ ├── mini-autobind.ts │ └── random │ ├── CryptoGen.ts │ ├── chacha20.ts │ ├── genrng.ts │ ├── randomBase.ts │ └── seedrandom.ts ├── test ├── index.ts ├── interpreter.ts ├── jump-statements.ts ├── keywords.ts ├── literals.ts ├── newline.ts ├── parser.ts ├── primitive-props.ts ├── std.ts ├── syntax.ts ├── testutils.ts └── types.ts ├── translations └── en │ ├── CHANGELOG.md │ ├── CONTRIBUTING.md │ ├── README.md │ └── docs │ └── README.md ├── tsconfig.json ├── unreleased ├── .gitkeep ├── IEEE 754 compliance around NaN.md ├── accept-multi-new-line.md ├── assign-left-eval-once.md ├── attr-under-ns.md ├── braces-in-template-expression.md ├── callstack.md ├── date-parse-err.md ├── destr-define.md ├── irq-config.md ├── jump-statements.md ├── match-sep-before-default.md ├── new-lines-in-template-expression.md ├── next-past.md ├── obj-extract ├── object-key-string.md ├── optional-args.md ├── pause.md ├── random algorithms.md ├── rename-arg-param.md ├── script-file.md ├── single-statement-clause-scope.md ├── strictify-types.md ├── unary-sign-operators.md ├── unified-type-annotation.md └── while.md └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | charset = utf-8 7 | insert_final_newline = true 8 | 9 | [*.yml] 10 | indent_style = space 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | groups: 13 | gh-actions: 14 | patterns: 15 | - "*" 16 | - package-ecosystem: "npm" # See documentation for possible values 17 | directory: "/" # Location of package manifests 18 | schedule: 19 | interval: "daily" 20 | groups: 21 | npm-deps: 22 | patterns: 23 | - "*" 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 15 | 29 | 30 | # What 31 | 32 | 33 | 34 | # Why 35 | 36 | 37 | 38 | # Additional info (optional) 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/api.yml: -------------------------------------------------------------------------------- 1 | name: API report 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | report: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4.2.2 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4.1.0 21 | with: 22 | node-version: 20.x 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Build 28 | run: npm run build 29 | 30 | - name: Check files 31 | run: ls built 32 | 33 | - name: API report 34 | run: npm run api-prod 35 | 36 | - name: Show report 37 | if: always() 38 | run: cat temp/aiscript.api.md 39 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4.2.2 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4.1.0 19 | with: 20 | node-version: 20.x 21 | 22 | - name: AiScript Cache dependencies 23 | uses: actions/cache@v4 24 | with: 25 | path: ~/.npm 26 | key: npm-${{ hashFiles('package-lock.json') }} 27 | restore-keys: npm- 28 | 29 | - name: AiScript Install dependencies 30 | run: npm ci 31 | 32 | - name: AiScript Build 33 | run: npm run build 34 | 35 | - name: Cache dependencies 36 | uses: actions/cache@v4 37 | with: 38 | path: ~/.npm 39 | key: npm-playground-${{ hashFiles('playground/package-lock.json') }} 40 | restore-keys: npm-playground- 41 | 42 | - name: Install dependencies 43 | run: npm ci 44 | working-directory: ./playground 45 | 46 | - name: Build 47 | run: npm run build 48 | working-directory: ./playground 49 | 50 | - name: Deploy 51 | uses: peaceiris/actions-gh-pages@v4 52 | if: ${{ github.ref == 'refs/heads/master' }} 53 | with: 54 | github_token: ${{ secrets.GITHUB_TOKEN }} 55 | publish_dir: ./playground/dist 56 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4.2.2 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4.1.0 21 | with: 22 | node-version: 20.x 23 | 24 | - name: Install dependencies 25 | run: npm ci 26 | 27 | - name: Lint 28 | run: npm run lint 29 | -------------------------------------------------------------------------------- /.github/workflows/publish-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Publish nightly 2 | 3 | on: 4 | schedule: 5 | - cron: '50 18 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | get-branches: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | matrix: ${{ steps.getb.outputs.matrix }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4.2.2 16 | with: 17 | fetch-depth: 0 18 | 19 | - id: getb 20 | run: | 21 | declare -A branches=( 22 | ["dev"]="master" 23 | ["next"]="aiscript-next" 24 | ) 25 | matrix='{"include":[' 26 | sep="" 27 | for tag in "${!branches[@]}"; do 28 | branch=${branches[${tag}]} 29 | if git show-ref --quiet refs/remotes/origin/${branch}; then 30 | matrix="${matrix}${sep}{\"branch\":\"${branch}\",\"tag\":\"${tag}\"}" 31 | sep="," 32 | fi 33 | done 34 | matrix="${matrix}]}" 35 | echo "matrix=$matrix" >> $GITHUB_OUTPUT 36 | 37 | 38 | publish: 39 | runs-on: ubuntu-latest 40 | needs: get-branches 41 | strategy: 42 | fail-fast: false 43 | matrix: ${{ fromJSON(needs.get-branches.outputs.matrix) }} 44 | env: 45 | NPM_SECRET: ${{ secrets.NPM_SECRET }} 46 | 47 | steps: 48 | - name: Checkout ${{ matrix.branch }} 49 | uses: actions/checkout@v4.2.2 50 | with: 51 | ref: ${{ matrix.branch }} 52 | 53 | - name: Setup Node.js 54 | uses: actions/setup-node@v4.1.0 55 | with: 56 | node-version: 20.x 57 | 58 | - name: Cache dependencies 59 | uses: actions/cache@v4 60 | with: 61 | path: ~/.npm 62 | key: npm-${{ hashFiles('package-lock.json') }} 63 | restore-keys: npm- 64 | 65 | - name: Install dependencies 66 | run: npm ci 67 | 68 | - name: Set Version 69 | run: | 70 | CURRENT_VER=$(npm view 'file:.' version) 71 | TIME_STAMP=$( date +'%Y%m%d' ) 72 | echo 'NEWVERSION='$CURRENT_VER-${{ matrix.tag }}.$TIME_STAMP >> $GITHUB_ENV 73 | 74 | - name: Check Commits 75 | run: | 76 | echo 'LAST_COMMITS='$( git log --since '24 hours ago' | wc -c ) >> $GITHUB_ENV 77 | 78 | - name: Prepare Publish 79 | run: npm run pre-release 80 | 81 | - name: Publish 82 | uses: JS-DevTools/npm-publish@v3 83 | if: ${{ env.NPM_SECRET != '' && env.LAST_COMMITS != 0 }} 84 | with: 85 | token: ${{ env.NPM_SECRET }} 86 | tag: ${{ matrix.tag }} 87 | access: public 88 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [20.x] 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4.2.2 22 | 23 | - name: Setup Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4.1.0 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | 31 | - name: Build 32 | run: npm run build 33 | 34 | - name: Test 35 | run: npm test -- --coverage 36 | env: 37 | CI: true 38 | 39 | - name: Upload Coverage 40 | uses: codecov/codecov-action@v4 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | /.vscode 4 | /coverage 5 | /temp 6 | /src/pkg.ts 7 | /src/parser/parser.js 8 | /src/parser/parser.mjs 9 | npm-debug.log 10 | main.ais 11 | pnpm-lock.yaml 12 | tsdoc-metadata.json 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /src 3 | /test 4 | /coverage 5 | /.github 6 | /playground 7 | /temp 8 | .gitignore 9 | npm-debug.log 10 | gulpfile.js 11 | tsconfig.json 12 | .editorconfig 13 | .travis.yml 14 | test.is 15 | tsdoc-metadata.json 16 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [Read translated version (en)](./translations/en/CHANGELOG.md) 2 | 3 | # 0.19.0 4 | 5 | - `Date:year`系の関数に0を渡すと現在時刻になる問題を修正 6 | - シンタックスエラーなどの位置情報を修正 7 | - `arr.reduce`が空配列に対して初期値なしで呼び出された時、正式にエラーを出すよう 8 | - `str.pad_start`,`str.pad_end`を追加 9 | - `arr.insert`,`arr.remove`を追加 10 | - `arr.sort`の処理を非同期的にして高速化 11 | - `arr.flat`,`arr.flat_map`を追加 12 | - `Uri:encode_full`, `Uri:encode_component`, `Uri:decode_full`, `Uri:decode_component`を追加 13 | - `str.starts_with`,`str.ends_with`を追加 14 | - `arr.splice`を追加 15 | - `arr.at`を追加 16 | - For Hosts: エラーハンドラ使用時、InterpreterのオプションでabortOnErrorをtrueにした時のみ全体のabortを行うように 17 | 18 | # 0.18.0 19 | - `Core:abort`でプログラムを緊急停止できるように 20 | - `index_of`の配列版を追加 21 | - `str.index_of` `arr.index_of`共に第2引数fromIndexを受け付けるように 22 | - `arr.incl`の引数の型制限を廃止 23 | - `Date:millisecond`を追加 24 | - `arr.fill`, `arr.repeat`, `Arr:create`を追加 25 | - JavaScriptのように分割代入ができるように(現段階では機能は最小限) 26 | - スコープおよび名前が同一である変数が宣言された際のエラーメッセージを修正 27 | - ネストされた名前空間下の変数を参照できるように 28 | - `arr.every`, `arr.some`を追加 29 | - `Date:to_iso_str`を追加 30 | 31 | # 0.17.0 32 | - `package.json`を修正 33 | - `Error:create`関数でエラー型の値を生成できるように 34 | - `Obj:merge`で2つのオブジェクトの併合を得られるように 35 | - Fix: チェイン系(インデックスアクセス`[]`、プロパティアクセス`.`、関数呼び出し`()`)と括弧を組み合わせた時に不正な挙動をするバグを修正 36 | - 関数`Str#charcode_at` `Str#to_arr` `Str#to_char_arr` `Str#to_charcode_arr` `Str#to_utf8_byte_arr` `Str#to_unicode_codepoint_arr` `Str:from_unicode_codepoints` `Str:from_utf8_bytes`を追加 37 | - Fix: `Str#codepoint_at`がサロゲートペアに対応していないのを修正 38 | - 配列の範囲外および非整数のインデックスへの代入でエラーを出すように 39 | ## Note 40 | バージョン0.16.0に記録漏れがありました。 41 | >- 関数`Str:from_codepoint` `Str#codepoint_at`を追加 42 | 43 | # 0.16.0 44 | - **ネームスペースのトップレベルに`var`は定義できなくなりました。(`let`は可能)** 45 | - `Core:to_str`, `テンプレート文字列` でどの値でも文字列へ変換できるように 46 | - 指定時間待機する関数`Core:sleep`を追加 47 | - `exists 変数名` の構文で変数が存在するか判定できるように 48 | - オブジェクトを添字で参照できるように(`object['index']`のように) 49 | - 「エラー型(`error`)」を導入 50 | - `Json:parse`がパース失敗時にエラー型の値を返すように 51 | - `let` で定義した変数が上書きできてしまうのを修正 52 | - 関数`Str:from_codepoint` `Str#codepoint_at`を追加 53 | 54 | ## For Hosts 55 | - **Breaking Change** AiScriptErrorのサブクラス4種にAiScript-の接頭辞を追加(例:SyntaxError→AiScriptSyntaxError) 56 | - Interpreterのコンストラクタの第2引数の要素に`err`(エラーコールバック)を設定できる。これは`Interpreter.exec`が失敗した時に加えて、**`Async:interval`や`Async:timeout`が失敗した場合にも呼び出される。** なお、これを設定した場合は例外throwは発生しなくなる。 57 | - ネイティブ関数は`opts.call`の代わりに`opts.topCall`を用いることで上記2つのようにエラーコールバックが呼び出されるように。**必要な場合にのみ使うこと。従来エラーキャッチ出来ていたケースでは引き続き`opts.call`を使う。** 58 | 59 | # 0.15.0 60 | - Mathを強化 61 | - `&&`, `||` 演算子の項が正しく変換されない可能性のあるバグを修正 62 | 63 | # 0.14.1 64 | 65 | - `&&`, `||` が短絡評価されないバグを修正 66 | - `+=`, `-=` 演算子で関係のない変数が上書きされる可能性のあるバグを修正 67 | 68 | # 0.14.0 69 | 70 | - オブジェクトの値を配列化する`Obj:vals`を追加 71 | - 文字列が`Json:parse`でパース可能であるかを判定する関数`Json:parsable`を追加 72 | - or/andの結果が第一引数で確定する時、第二引数を評価しないように 73 | - Fix immediate value check in Async:interval 74 | 75 | # 0.13.3 76 | - 乱数を生成するとき引数の最大値を戻り値に含むように 77 | 78 | # 0.13.2 79 | - `Date:year`,`Date:month`,`Date:day`,`Date:hour`,`Date:minute`,`Date:second`に時間数値の引数を渡して時刻指定可能に 80 | - array.sortとString用比較関数Str:lt, Str:gtの追加 81 | - 乱数を生成するとき引数の最大値を戻り値に含むように 82 | 83 | # 0.13.1 84 | - Json:stringifyに関数を渡すと不正な値が生成されるのを修正 85 | 86 | # 0.13.0 87 | - 配列プロパティ`map`,`filter`,`reduce`,`find`に渡すコールバック関数が受け取るインデックスを0始まりに 88 | - `@Math:ceil(x: num): num` を追加 89 | - 冪乗の `Core:pow` とその糖衣構文 `^` 90 | - 少数のパースを修正 91 | 92 | # 0.12.4 93 | - block comment `/* ... */` 94 | - Math:Infinity 95 | 96 | # 0.12.3 97 | - each文の中でbreakとreturnが動作しない問題を修正 98 | - 配列の境界外にアクセスした際にIndexOutOfRangeエラーを発生させるように 99 | 100 | # 0.12.2 101 | - 否定構文`!` 102 | - インタプリタ処理速度の調整 103 | 104 | # 0.12.1 105 | - 文字列をシングルクォートでも定義可能に 106 | - for文、loop文の中でreturnが動作しない問題を修正 107 | - 無限ループ時にランタイムがフリーズしないように 108 | 109 | # 0.12.0 110 | ## Breaking changes 111 | - 変数定義の`#` → `let` 112 | - 変数定義の`$` → `var` 113 | - 代入の`<-` → `=` 114 | - 比較の`=` → `==` 115 | - `&` → `&&` 116 | - `|` → `||` 117 | - `? ~ .? ~ .` → `if ~ elif ~ else` 118 | - `? x { 42 => yes }` → `match x { 42 => true }` 119 | - `yes` `no` → `true` `false` 120 | - `_` → `null` 121 | - `<<` → `return` 122 | - `~` → `for` 123 | - `~~` → `each` 124 | - `+ attributeName attributeValue` → `#[attributeName attributeValue]` 125 | - 真理値の`+`/`-`表記方法を廃止 126 | - for、およびeachは配列を返さなくなりました 127 | - ブロック式は`{ }`→`eval { }`に 128 | - 配列のインデックスは0始まりになりました 129 | - いくつかのstdに含まれるメソッドは、対象の値のプロパティとして利用するようになりました。例: 130 | - `Str:to_num("123")` -> `"123".to_num()` 131 | - `Arr:len([1 2 3])` -> `[1 2 3].len` 132 | - etc 133 | 134 | ## Features 135 | - `continue` 136 | - `break` 137 | - `loop` 138 | 139 | ## Fixes 140 | - 空の関数を定義できない問題を修正 141 | - 空のスクリプトが許可されていない問題を修正 142 | - ネームスペース付き変数のインクリメント、デクリメントを修正 143 | - ネームスペース付き変数への代入ができない問題を修正 144 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | syuilotan@yahoo.co.jp. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [Read translated version (en)](./translations/en/CONTRIBUTING.md) 2 | 3 | # Contribution guide 4 | プロジェクトに興味を持っていただきありがとうございます! 5 | このドキュメントでは、プロジェクトに貢献する際に必要な情報をまとめています。 6 | 7 | ## 実装をする前に 8 | 機能追加やバグ修正をしたいときは、まずIssueなどで設計、方針をレビューしてもらいましょう(無い場合は作ってください)。このステップがないと、せっかく実装してもPRがマージされない可能性が高くなります。 9 | 10 | また、実装に取り掛かるときは当該Issueに自分をアサインしてください(自分でできない場合は他メンバーに自分をアサインしてもらうようお願いしてください)。 11 | 自分が実装するという意思表示をすることで、作業がバッティングするのを防ぎます。 12 | 13 | 言語仕様の改変がある場合は、テストおよび[ドキュメント](https://github.com/aiscript-dev/aiscript-dev.github.io)の更新も同時にお願いします。 14 | 15 | ## Tools 16 | ### Vitest 17 | このプロジェクトではテストフレームワークとして[Vitest](https://vitest.dev)を導入しています。 18 | テストは[`/test`ディレクトリ](./test)に置かれます。 19 | 20 | テストはCIにより各コミット/各PRに対して自動で実施されます。 21 | ローカル環境でテストを実施するには、`npm run test`を実行してください。 22 | 23 | ### API Extractor 24 | このプロジェクトでは[API Extractor](https://api-extractor.com/)を導入しています。API ExtractorはAPIレポートを生成する役割を持ちます。 25 | APIレポートはいわばAPIのスナップショットで、このライブラリが外部に公開(export)している各種関数や型の定義が含まれています。`npm run api`コマンドを実行すると、その時点でのレポートが[`/etc`ディレクトリ](./etc)に生成されるようになっています。 26 | 27 | exportしているAPIに変更があると、当然生成されるレポートの内容も変わるので、例えばdevelopブランチで生成されたレポートとPRのブランチで生成されたレポートを比較することで、意図しない破壊的変更の検出や、破壊的変更の影響確認に用いることができます。 28 | また、各コミットや各PRで実行されるCI内部では、都度APIレポートを生成して既存のレポートと差分が無いかチェックしています。もし差分があるとエラーになります。 29 | 30 | PRを作る際は、`npm run api`コマンドを実行してAPIレポートを生成し、差分がある場合はコミットしてください。 31 | レポートをコミットすることでその破壊的変更が意図したものであると示すことができるほか、上述したようにレポート間の差分が出ることで影響範囲をレビューしやすくなります。 32 | 33 | ### Codecov 34 | このプロジェクトではカバレッジの計測に[Codecov](https://about.codecov.io/)を導入しています。カバレッジは、コードがどれくらいテストでカバーされているかを表すものです。 35 | 36 | カバレッジ計測はCIで自動的に行われ、特に操作は必要ありません。カバレッジは[ここ](https://codecov.io/gh/syuilo/aiscript)から見ることができます。 37 | 38 | また、各PRに対してもそのブランチのカバレッジが自動的に計算され、マージ先のカバレッジとの差分を含んだレポートがCodecovのbotによりコメントされます。これにより、そのPRをマージすることでどれくらいカバレッジが増加するのか/減少するのかを確認することができます。 39 | 40 | ## レビュイーの心得 41 | PRを作成するときのテンプレートに色々書いてあるので読んでみてください。(このドキュメントに移してもいいかも?) 42 | また、後述の「レビュー観点」も意識してみてください。 43 | 44 | ## レビュワーの心得 45 | - 直して欲しい点だけでなく、良い点も積極的にコメントしましょう。 46 | - 貢献するモチベーションアップに繋がります。 47 | 48 | ### レビュー観点 49 | - セキュリティ 50 | - このPRをマージすることで、脆弱性を生まないか? 51 | - パフォーマンス 52 | - このPRをマージすることで、予期せずパフォーマンスが悪化しないか? 53 | - もっと効率的な方法は無いか? 54 | - テスト 55 | - 期待する振る舞いがテストで担保されているか? 56 | - 抜けやモレは無いか? 57 | - 異常系のチェックは出来ているか? 58 | 59 | ## PRマージ時の規則 60 | 以降の規則は[member](https://github.com/orgs/aiscript-dev/people)が何をしてよいか明示するものであり、何かを禁止するものではありません。(禁止のための条項が必要になれば別に作ります) 61 | - バグ修正、ドキュメントの編集、バージョン更新、dependabotのPRはmember1人の判断でマージしてよい 62 | - ソースコード、テスト、本規則自体の変更は、member 2人以上のapproveを受けてから1日の経過を待ち、非memberを含む反対者が賛成者の半数以下ならマージしてよい 63 | - 上記以外の変更はmember 1人以上のapproveを受けてから1日の経過を待ち、非memberを含む反対者が賛成者の半数以下ならマージしてよい 64 | - リポジトリの所有者(@syuilo)の同意がある場合、以上の規則によらずにマージしてもよい 65 | - 大前提としてすべてのマージはCI Checkを通過し、全てのreviewにPR主から何らかの返答がある必要がある 66 | - 後でrevertになっても泣かない 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 syuilo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

AiScript

2 | 3 | [![](https://img.shields.io/npm/v/@syuilo/aiscript.svg?style=flat-square)](https://www.npmjs.com/package/@syuilo/aiscript) 4 | [![Test](https://github.com/syuilo/aiscript/actions/workflows/test.yml/badge.svg)](https://github.com/syuilo/aiscript/actions/workflows/test.yml) 5 | [![codecov](https://codecov.io/gh/syuilo/aiscript/branch/master/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/syuilo/aiscript) 6 | [![](https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square)](http://opensource.org/licenses/MIT) 7 | [![](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&logo=github)](http://makeapullrequest.com) 8 | 9 | > **AiScript** is a lightweight scripting language that runs on JavaScript. 10 | 11 | [Play online ▶](https://aiscript-dev.github.io/ja/playground.html) 12 | 13 | AiScriptは、JavaScript上で動作する軽量スクリプト言語です。 14 | 15 | * 配列、オブジェクト、関数等をファーストクラスでサポート 16 | * JavaScript風構文で書きやすい 17 | * セキュアなサンドボックス環境で実行される 18 | * 無限ループ等でもホストをフリーズさせない 19 | * ホストから変数や関数を簡単に提供可能 20 | 21 | このリポジトリには、JavaScriptで実装されたパーサーと処理系が含まれます。 22 | 23 | [ドキュメントはこちらをご覧ください](https://aiscript-dev.github.io/ja/) 24 | 25 | [Read translated document](https://aiscript-dev.github.io/en/) 26 | 27 | ## Getting started (language) 28 | [はじめに(日本語)](https://aiscript-dev.github.io/ja/guides/get-started.html) 29 | 30 | [Get Started (en)](https://aiscript-dev.github.io/en/guides/get-started.html) 31 | 32 | ## Getting started (host implementation) 33 | [アプリに組み込む(日本語)](https://aiscript-dev.github.io/ja/guides/implementation.html) 34 | 35 | [Embedding into Your Application (en)](https://aiscript-dev.github.io/en/guides/implementation.html) 36 | 37 | ## Example programs 38 | ### Hello world 39 | ``` 40 | <: "Hello, world!" 41 | ``` 42 | 43 | ### Fizz Buzz 44 | ``` 45 | for (let i, 100) { 46 | <: if (i % 15 == 0) "FizzBuzz" 47 | elif (i % 3 == 0) "Fizz" 48 | elif (i % 5 == 0) "Buzz" 49 | else i 50 | } 51 | ``` 52 | 53 | ## License 54 | [MIT](LICENSE) 55 | -------------------------------------------------------------------------------- /aiscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiscript-dev/aiscript/f6670a79cf4946b927950aac39c0bc5edfb98994/aiscript.png -------------------------------------------------------------------------------- /api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | 4 | "mainEntryPointFilePath": "/built/dts/index.d.ts", 5 | 6 | "bundledPackages": [], 7 | 8 | "compiler": { 9 | }, 10 | 11 | "apiReport": { 12 | "enabled": true 13 | }, 14 | 15 | "docModel": { 16 | "enabled": true 17 | }, 18 | 19 | "dtsRollup": { 20 | "enabled": false 21 | }, 22 | 23 | "tsdocMetadata": { 24 | }, 25 | 26 | "messages": { 27 | "compilerMessageReporting": { 28 | "default": { 29 | "logLevel": "warning" 30 | } 31 | }, 32 | 33 | "extractorMessageReporting": { 34 | "default": { 35 | "logLevel": "none" 36 | }, 37 | 38 | "ae-wrong-input-file-type": { 39 | "logLevel": "none" 40 | } 41 | }, 42 | 43 | "tsdocMessageReporting": { 44 | "default": { 45 | "logLevel": "warning" 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: false 4 | patch: false 5 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ドキュメント移動済み 2 | 本ディレクトリ下にあったドキュメントは、開発用のものを除き専用Webサイトに移動されました。 3 | 4 | [該当issue](https://github.com/aiscript-dev/aiscript/issues/804) 5 | 6 | [移行先サイト](https://aiscript-dev.github.io/ja/) 7 | 8 | [移行先サイトのリポジトリ](https://github.com/aiscript-dev/aiscript-dev.github.io) 9 | -------------------------------------------------------------------------------- /docs/parser/overview.md: -------------------------------------------------------------------------------- 1 | # AiScriptパーサーの全体像 2 | 3 | AiScriptのパーサーは2つの段階を経て構文ツリーに変換される。 4 | 5 | 1. ソースコードをトークン列に分割する 6 | 2. トークン列を順番に読み取って構文ツリー(AST)を構築する 7 | 8 | ソースコードをトークン列に分割する処理(トークナイズと呼ばれる)は「Scanner」というモジュールが担当する。 9 | トークン列から構文ツリーを構築する処理(パース)は、syntaxesディレクトリ以下にあるパース関数が担当する。名前がparseから始まっている関数がパース関数。 10 | 11 | AiScriptのパーサーではトークナイズはまとめて行われない。 12 | パース関数が次のトークンを要求すると、下位モジュールであるScannerが次のトークンを1つだけ読み取る。 13 | 14 | Scannerによって現在の読み取り位置(カーソル位置)が保持される。 15 | また、Scannerの各種メソッドで現在のトークンが期待されたものと一致するかどうかの確認やトークンの種類の取得などを行える。 16 | これらの機能を利用することにより、パース関数を簡潔に記述できる。 17 | -------------------------------------------------------------------------------- /docs/parser/scanner.md: -------------------------------------------------------------------------------- 1 | # Scanner 設計資料 2 | 3 | ## 現在のトークンと先読みされたトークン 4 | _tokensの0番には現在のトークンが保持される。また、トークンが先読みされた場合は1番以降にそれらのトークンが保持されていくことになる。 5 | 例えば、次のトークンを1つ先読みした場合は0番に現在のトークンが入り1番に先読みされたトークンが入る。 6 | 7 | nextメソッドで現在位置が移動すると、それまで0番にあったトークン(現在のトークン)は配列から削除され、1番にあった要素は現在のトークンとなる。 8 | 配列から全てのトークンが無くなった場合はトークンの読み取りが実行される。 9 | 10 | ## CharStream 11 | ScannerはCharStreamを下位モジュールとして利用する。 12 | CharStreamは入力文字列から一文字ずつ文字を取り出すことができる。 13 | -------------------------------------------------------------------------------- /docs/parser/token-streams.md: -------------------------------------------------------------------------------- 1 | # TokenStreams 2 | 各種パース関数はITokenStreamインターフェースを実装したクラスインスタンスを引数にとる。 3 | 4 | 実装クラス 5 | - Scanner 6 | - TokenStream 7 | 8 | ## TokenStream 9 | 読み取り済みのトークン列を入力にとるストリーム。 10 | テンプレート構文の式部分ではトークン列の読み取りだけを先に行い、式の内容の解析はパース時に遅延して行われる。 11 | この時の読み取り済みのトークン列はTokenStremとしてパース関数に渡される。 12 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import importPlugin from "eslint-plugin-import"; 2 | import js from "@eslint/js"; 3 | import ts from 'typescript-eslint'; 4 | 5 | export default ts.config({ 6 | // https://stackoverflow.com/a/79115209/22200513 7 | ignores: ["**/*.js"] 8 | }, { 9 | files: ["src/**/*.ts"], 10 | extends: [ 11 | js.configs.recommended, 12 | ...ts.configs.recommended, 13 | importPlugin.flatConfigs.recommended, 14 | importPlugin.flatConfigs.typescript, 15 | ], 16 | 17 | languageOptions: { 18 | ecmaVersion: 5, 19 | sourceType: "script", 20 | 21 | parserOptions: { 22 | tsconfigRootDir: ".", 23 | project: ["./tsconfig.json"], 24 | }, 25 | }, 26 | 27 | rules: { 28 | indent: ["warn", "tab", { 29 | SwitchCase: 1, 30 | MemberExpression: 1, 31 | flatTernaryExpressions: true, 32 | ArrayExpression: "first", 33 | ObjectExpression: "first", 34 | }], 35 | 36 | "eol-last": ["error", "always"], 37 | semi: ["error", "always"], 38 | 39 | "semi-spacing": ["error", { 40 | before: false, 41 | after: true, 42 | }], 43 | 44 | quotes: ["warn", "single"], 45 | "comma-dangle": ["warn", "always-multiline"], 46 | 47 | "keyword-spacing": ["error", { 48 | before: true, 49 | after: true, 50 | }], 51 | 52 | "key-spacing": ["error", { 53 | beforeColon: false, 54 | afterColon: true, 55 | }], 56 | 57 | "arrow-spacing": ["error", { 58 | before: true, 59 | after: true, 60 | }], 61 | 62 | "padded-blocks": ["error", "never"], 63 | 64 | eqeqeq: ["error", "always", { 65 | null: "ignore", 66 | }], 67 | 68 | "no-multi-spaces": ["error"], 69 | "no-var": ["error"], 70 | "prefer-arrow-callback": ["error"], 71 | "no-throw-literal": ["warn"], 72 | "no-param-reassign": ["warn"], 73 | "no-constant-condition": ["warn", { "checkLoops": "all" }], 74 | "no-empty-pattern": ["warn"], 75 | "no-async-promise-executor": ["off"], 76 | "no-useless-escape": ["off"], 77 | 78 | "no-multiple-empty-lines": ["error", { 79 | max: 1, 80 | }], 81 | 82 | "no-control-regex": ["warn"], 83 | "no-empty": ["warn"], 84 | "no-inner-declarations": ["off"], 85 | "no-sparse-arrays": ["off"], 86 | "nonblock-statement-body-position": ["error", "beside"], 87 | "object-curly-spacing": ["error", "always"], 88 | "space-infix-ops": ["error"], 89 | "space-before-blocks": ["error", "always"], 90 | "@typescript-eslint/no-explicit-any": ["warn"], 91 | "@typescript-eslint/no-unnecessary-condition": ["warn"], 92 | "@typescript-eslint/no-var-requires": ["warn"], 93 | "@typescript-eslint/no-inferrable-types": ["warn"], 94 | "@typescript-eslint/no-empty-function": ["off"], 95 | "@typescript-eslint/no-non-null-assertion": ["off"], 96 | "@typescript-eslint/explicit-function-return-type": ["warn"], 97 | 98 | "@typescript-eslint/no-misused-promises": ["error", { 99 | checksVoidReturn: false, 100 | }], 101 | 102 | "@typescript-eslint/consistent-type-imports": "error", 103 | "import/no-unresolved": ["off"], 104 | "import/no-default-export": ["warn"], 105 | 106 | "import/order": ["warn", { 107 | groups: [ 108 | "builtin", 109 | "external", 110 | "internal", 111 | "parent", 112 | "sibling", 113 | "index", 114 | "object", 115 | "type", 116 | ], 117 | }], 118 | 119 | "@typescript-eslint/no-unused-vars": ["warn", { 120 | argsIgnorePattern: "^_", 121 | varsIgnorePattern: "^_", 122 | caughtErrorsIgnorePattern: "^_", 123 | destructuredArrayIgnorePattern: "^_", 124 | }], 125 | }, 126 | }); 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "@syuilo/aiscript", 4 | "version": "1.0.0", 5 | "description": "AiScript implementation", 6 | "author": "syuilo ", 7 | "license": "MIT", 8 | "repository": "https://github.com/syuilo/aiscript.git", 9 | "homepage": "https://aiscript-dev.github.io/", 10 | "bugs": "https://github.com/syuilo/aiscript/issues", 11 | "exports": { 12 | ".": { 13 | "import": "./built/esm/index.js", 14 | "types": "./built/dts/index.d.ts" 15 | }, 16 | "./*": { 17 | "import": "./built/esm/*", 18 | "types": "./built/dts/*" 19 | } 20 | }, 21 | "scripts": { 22 | "start": "node ./scripts/start.mjs", 23 | "parse": "node ./scripts/parse.mjs", 24 | "repl": "node ./scripts/repl.mjs", 25 | "ts": "npm run ts-esm && npm run ts-dts", 26 | "ts-esm": "tsc --outDir built/esm", 27 | "ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true", 28 | "build": "node scripts/gen-pkg-ts.mjs && npm run ts", 29 | "api": "npx api-extractor run --local --verbose", 30 | "api-prod": "npx api-extractor run --verbose", 31 | "lint": "eslint .", 32 | "test": "vitest run", 33 | "pre-release": "node scripts/pre-release.mjs && npm run build", 34 | "prepublishOnly": "node scripts/check-release.mjs" 35 | }, 36 | "devDependencies": { 37 | "@eslint/js": "9.17.0", 38 | "@microsoft/api-extractor": "7.48.1", 39 | "@types/eslint__js": "8.42.3", 40 | "@types/node": "22.10.2", 41 | "@types/seedrandom": "3.0.8", 42 | "@vitest/coverage-v8": "2.1.8", 43 | "chalk": "5.4.1", 44 | "eslint": "9.17.0", 45 | "eslint-plugin-import": "2.31.0", 46 | "semver": "7.6.3", 47 | "ts-node": "10.9.2", 48 | "typescript": "5.7.2", 49 | "typescript-eslint": "8.18.2", 50 | "vitest": "2.1.8" 51 | }, 52 | "dependencies": { 53 | "seedrandom": "3.0.5", 54 | "stringz": "2.1.0", 55 | "uuid": "11.0.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AiScript Playground 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "aiscript-playground", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@syuilo/aiscript": "file:..", 12 | "prismjs": "^1.29.0", 13 | "vite": "^5.4.8", 14 | "vue": "^3.5.11", 15 | "vue-prism-editor": "^2.0.0-alpha.2" 16 | }, 17 | "devDependencies": { 18 | "@vitejs/plugin-vue": "^5.1.4", 19 | "@vue/compiler-sfc": "^3.0.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /playground/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiscript-dev/aiscript/f6670a79cf4946b927950aac39c0bc5edfb98994/playground/public/.gitkeep -------------------------------------------------------------------------------- /playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 148 | 149 | 273 | -------------------------------------------------------------------------------- /playground/src/Settings.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | 37 | 41 | 42 | 60 | -------------------------------------------------------------------------------- /playground/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiscript-dev/aiscript/f6670a79cf4946b927950aac39c0bc5edfb98994/playground/src/assets/.gitkeep -------------------------------------------------------------------------------- /playground/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /playground/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { readFileSync } from 'fs'; 4 | 5 | const data = JSON.parse(readFileSync('../tsconfig.json', 'utf8')); 6 | 7 | let target = data.compilerOptions.target; 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | base: './', 12 | plugins: [vue()], 13 | server: { 14 | fs: { 15 | allow: [ '..' ] 16 | } 17 | }, 18 | build: { 19 | target: target 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /scripts/check-release.mjs: -------------------------------------------------------------------------------- 1 | import { readdir } from 'node:fs/promises'; 2 | 3 | await readdir('./unreleased') 4 | .then(pathes => { 5 | if (pathes.length > 1 || (pathes.length === 1 && pathes[0] !== '.gitkeep')) throw new Error('Run "npm run pre-release" before publish.') 6 | }, err => { 7 | if (err.code !== 'ENOENT') throw err; 8 | }); 9 | -------------------------------------------------------------------------------- /scripts/gen-pkg-ts.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises'; 2 | 3 | const pkg = JSON.parse((await readFile('./package.json', 'utf8'))); 4 | await writeFile('./src/pkg.ts', 5 | `/* 6 | This file is automatically generated by scripts/gen-pkg-ts.js. 7 | DO NOT edit this file. To change it's behavior, edit scripts/gen-pkg-ts.js instead. 8 | */ 9 | /* eslint-disable quotes */ 10 | /* eslint-disable comma-dangle */ 11 | export const pkg = ${ JSON.stringify({ 12 | version: pkg.version, 13 | }, undefined, '\t')} as const; 14 | ` 15 | ); 16 | -------------------------------------------------------------------------------- /scripts/parse.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { Parser } from '@syuilo/aiscript'; 3 | import { inspect } from 'util'; 4 | 5 | const script = fs.readFileSync('./main.ais', 'utf8'); 6 | const ast = Parser.parse(script); 7 | console.log(inspect(ast, { depth: 10 })); 8 | -------------------------------------------------------------------------------- /scripts/pre-release.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, readdir, writeFile, mkdir, rm } from 'node:fs/promises'; 2 | import { env } from 'node:process'; 3 | import { promisify } from 'node:util'; 4 | import child_process from 'node:child_process'; 5 | import semverValid from 'semver/functions/valid.js'; 6 | 7 | 8 | const exec = promisify(child_process.exec); 9 | const FILES = { 10 | chlog: './CHANGELOG.md', 11 | chlogs: './unreleased', 12 | pkgjson: './package.json', 13 | }; 14 | const enc = { encoding: env.ENCODING ?? 'utf8' }; 15 | const pkgjson = JSON.parse(await readFile(FILES.pkgjson, enc)); 16 | const newver = (() => { 17 | const newverCandidates = [ 18 | [env.NEWVERSION, 'Environment variable NEWVERSION'], 19 | [pkgjson.version, "Package.json's version field"], 20 | ]; 21 | for (const [ver, name] of newverCandidates) { 22 | if (ver) { 23 | if (semverValid(ver)) return ver; 24 | else throw new Error(`${name} is set to "${ver}"; it is not valid`); 25 | } 26 | } 27 | throw new Error('No effective version setting detected.'); 28 | })(); 29 | const actions = {}; 30 | 31 | /* 32 | * Update package.json's version field 33 | */ 34 | actions.updatePackageJson = { 35 | async read() { 36 | return JSON.stringify( 37 | { ...pkgjson, version: newver }, 38 | null, '\t' 39 | ); 40 | }, 41 | async write(json) { 42 | return writeFile(FILES.pkgjson, json); 43 | }, 44 | }; 45 | 46 | /* 47 | * Collect changelogs 48 | */ 49 | actions.collectChangeLogs = { 50 | async read() { 51 | const getNewLog = async () => { 52 | const pathes = (await readdir(FILES.chlogs)).map(path => `${FILES.chlogs}/${path}`); 53 | const pathesLastUpdate = await Promise.all( 54 | pathes.map(async (path) => { 55 | const gittime = Number((await exec( 56 | `git log -1 --pretty="format:%ct" "${path}"` 57 | )).stdout); 58 | if (gittime) return { path, lastUpdate: gittime }; 59 | else { 60 | console.log(`Warning: git timestamp of "${path}" was not detected`); 61 | return { path, lastUpdate: Infinity } 62 | } 63 | }) 64 | ); 65 | pathesLastUpdate.sort((a, b) => a.lastUpdate - b.lastUpdate); 66 | const logPromises = pathesLastUpdate.map(({ path }) => readFile(path, enc)); 67 | const logs = await Promise.all(logPromises); 68 | return logs.map(v => v.trim()).join('\n'); 69 | }; 70 | const getOldLog = async () => { 71 | const log = await readFile(FILES.chlog, enc); 72 | const idx = log.indexOf('#'); 73 | return [ 74 | log.slice(0, idx), 75 | log.slice(idx), 76 | ]; 77 | }; 78 | const [newLog, [logHead, oldLog]] = await Promise.all([ getNewLog(), getOldLog() ]); 79 | return `${logHead}# ${newver}\n${newLog}\n\n${oldLog}`; 80 | 81 | }, 82 | async write(logs) { 83 | return Promise.all([ 84 | writeFile(FILES.chlog, logs), 85 | rm(FILES.chlogs, { 86 | recursive: true, 87 | force: true, 88 | }).then(() => 89 | mkdir(FILES.chlogs) 90 | ).then(() => 91 | writeFile(`${FILES.chlogs}/.gitkeep`, '')) 92 | ]); 93 | }, 94 | }; 95 | 96 | // read all before writing 97 | 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}`) })])); 98 | 99 | // write after reading all 100 | await Promise.all(reads.map(([name, read]) => actions[name].write(read).catch(err => { throw new Error(`in actions.${name}.write: ${err}`) }))); 101 | 102 | -------------------------------------------------------------------------------- /scripts/repl.mjs: -------------------------------------------------------------------------------- 1 | import * as readline from 'readline/promises'; 2 | import chalk from 'chalk'; 3 | import { errors, Parser, Interpreter, utils } from '@syuilo/aiscript'; 4 | const { valToString } = utils; 5 | 6 | const i = readline.createInterface({ 7 | input: process.stdin, 8 | output: process.stdout 9 | }); 10 | 11 | console.log( 12 | `Welcome to AiScript! 13 | https://github.com/syuilo/aiscript 14 | 15 | Type '.exit' to end this session.`); 16 | 17 | const interpreter = new Interpreter({}, { 18 | in(q) { 19 | return i.question(q + ': '); 20 | }, 21 | out(value) { 22 | if (value.type === 'str') { 23 | console.log(chalk.magenta(value.value)); 24 | } else { 25 | console.log(chalk.magenta(valToString(value))); 26 | } 27 | }, 28 | err(e) { 29 | console.log(chalk.red(`${e}`)); 30 | }, 31 | log(type, params) { 32 | switch (type) { 33 | case 'end': console.log(chalk.gray(`< ${valToString(params.val, true)}`)); break; 34 | default: break; 35 | } 36 | } 37 | }); 38 | 39 | async function getAst() { 40 | let script = ''; 41 | let a = await i.question('>>> '); 42 | while (true) { 43 | try { 44 | if (a === '.exit') return null; 45 | script += a; 46 | let ast = Parser.parse(script); 47 | script = ''; 48 | return ast; 49 | } catch(e) { 50 | if (e instanceof errors.AiScriptUnexpectedEOFError) { 51 | script += '\n'; 52 | a = await i.question('... '); 53 | } else { 54 | script = ''; 55 | throw e; 56 | } 57 | } 58 | } 59 | } 60 | 61 | async function main(){ 62 | try { 63 | let ast = await getAst(); 64 | if (ast == null) { 65 | return false; 66 | } 67 | await interpreter.exec(ast); 68 | } catch(e) { 69 | console.log(chalk.red(`${e}`)); 70 | } 71 | return true; 72 | }; 73 | 74 | while (await main()); 75 | i.close(); 76 | -------------------------------------------------------------------------------- /scripts/start.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import * as readline from 'readline'; 3 | import chalk from 'chalk'; 4 | import { Parser, Interpreter, errors, utils } from '@syuilo/aiscript'; 5 | const { AiScriptError } = errors; 6 | const { valToString } = utils; 7 | 8 | const i = readline.createInterface({ 9 | input: process.stdin, 10 | output: process.stdout 11 | }); 12 | 13 | const interpreter = new Interpreter({}, { 14 | in(q) { 15 | return new Promise(ok => { 16 | i.question(q + ': ', ok); 17 | }); 18 | }, 19 | out(value) { 20 | console.log(chalk.magenta(valToString(value, true))); 21 | }, 22 | err(e) { 23 | console.log(chalk.red(`${e}`)); 24 | }, 25 | log(type, params) { 26 | /* 27 | switch (type) { 28 | case 'node': console.log(chalk.gray(`\t\t${nodeToString(params.node)}`)); break; 29 | case 'var:add': console.log(chalk.greenBright(`\t\t\t+ #${params.var} = ${valToString(params.val)}`)); break; 30 | case 'var:read': console.log(chalk.cyan(`\t\t\tREAD #${params.var} : ${valToString(params.val)}`)); break; 31 | case 'var:write': console.log(chalk.yellow(`\t\t\tWRITE #${params.var} = ${valToString(params.val)}`)); break; 32 | case 'block:enter': console.log(`\t-> ${params.scope}`); break; 33 | case 'block:return': console.log(`\t<< ${params.scope}: ${valToString(params.val)}`); break; 34 | case 'block:leave': console.log(`\t<- ${params.scope}: ${valToString(params.val)}`); break; 35 | case 'end': console.log(`\t= ${valToString(params.val)}`); break; 36 | default: break; 37 | } 38 | */ 39 | } 40 | }); 41 | 42 | const script = fs.readFileSync('./main.ais', 'utf8'); 43 | try { 44 | const ast = Parser.parse(script); 45 | await interpreter.exec(ast); 46 | } catch (e) { 47 | if (e instanceof AiScriptError) { 48 | console.log(chalk.red(`${e}`)); 49 | } else { 50 | throw e 51 | } 52 | } 53 | i.close(); 54 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | export const textEncoder = new TextEncoder(); 2 | export const textDecoder = new TextDecoder(); 3 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { pkg } from './pkg.js'; 2 | export const AISCRIPT_VERSION = pkg.version; 3 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import { TokenKind } from './parser/token.js'; 2 | import type { Pos } from './node.js'; 3 | 4 | export abstract class AiScriptError extends Error { 5 | // name is read by Error.prototype.toString 6 | public name = 'AiScript'; 7 | public info: unknown; 8 | public pos?: Pos; 9 | 10 | constructor(message: string, info?: unknown) { 11 | super(message); 12 | 13 | this.info = info; 14 | 15 | // Maintains proper stack trace for where our error was thrown (only available on V8) 16 | if (Error.captureStackTrace) { 17 | Error.captureStackTrace(this, AiScriptError); 18 | } 19 | } 20 | } 21 | 22 | /** 23 | * Wrapper for non-AiScript errors. 24 | */ 25 | export class NonAiScriptError extends AiScriptError { 26 | public name = 'Internal'; 27 | constructor(error: unknown) { 28 | const message = String( 29 | (error as { message?: unknown } | null | undefined)?.message ?? error, 30 | ); 31 | super(message, error); 32 | } 33 | } 34 | 35 | /** 36 | * Parse-time errors. 37 | */ 38 | export class AiScriptSyntaxError extends AiScriptError { 39 | public name = 'Syntax'; 40 | constructor(message: string, public pos: Pos, info?: unknown) { 41 | super(`${message} (Line ${pos.line}, Column ${pos.column})`, info); 42 | } 43 | } 44 | 45 | /** 46 | * Unexpected EOF errors. 47 | */ 48 | export class AiScriptUnexpectedEOFError extends AiScriptSyntaxError { 49 | constructor(pos: Pos, info?: unknown) { 50 | super('unexpected EOF', pos, info); 51 | } 52 | } 53 | 54 | /** 55 | * Type validation(parser/plugins/validate-type) errors. 56 | */ 57 | export class AiScriptTypeError extends AiScriptError { 58 | public name = 'Type'; 59 | constructor(message: string, public pos: Pos, info?: unknown) { 60 | super(`${message} (Line ${pos.line}, Column ${pos.column})`, info); 61 | } 62 | } 63 | 64 | /** 65 | * Namespace collection errors. 66 | */ 67 | export class AiScriptNamespaceError extends AiScriptError { 68 | public name = 'Namespace'; 69 | constructor(message: string, public pos: Pos, info?: unknown) { 70 | super(`${message} (Line ${pos.line}, Column ${pos.column})`, info); 71 | } 72 | } 73 | 74 | /** 75 | * Interpret-time errors. 76 | */ 77 | export class AiScriptRuntimeError extends AiScriptError { 78 | public name = 'Runtime'; 79 | constructor(message: string, info?: unknown) { 80 | super(message, info); 81 | } 82 | } 83 | /** 84 | * RuntimeError for illegal access to arrays. 85 | */ 86 | export class AiScriptIndexOutOfRangeError extends AiScriptRuntimeError { 87 | constructor(message: string, info?: unknown) { 88 | super(message, info); 89 | } 90 | } 91 | /** 92 | * Errors thrown by users. 93 | */ 94 | export class AiScriptUserError extends AiScriptRuntimeError { 95 | public name = ''; 96 | constructor(message: string, info?: unknown) { 97 | super(message, info); 98 | } 99 | } 100 | /** 101 | * Host side configuration errors. 102 | */ 103 | export class AiScriptHostsideError extends AiScriptError { 104 | public name = 'Host'; 105 | constructor(message: string, info?: unknown) { 106 | super(message, info); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // api-extractor not support yet 2 | //export * from './interpreter/index'; 3 | //export * as utils from './interpreter/util'; 4 | //export * as values from './interpreter/value'; 5 | import { Interpreter } from './interpreter/index.js'; 6 | import { Scope } from './interpreter/scope.js'; 7 | import * as utils from './interpreter/util.js'; 8 | import * as values from './interpreter/value.js'; 9 | import { Parser } from './parser/index.js'; 10 | import * as errors from './error.js'; 11 | import * as Ast from './node.js'; 12 | import { AISCRIPT_VERSION } from './constants.js'; 13 | import type { ParserPlugin, PluginType } from './parser/index.js'; 14 | export { Interpreter }; 15 | export { Scope }; 16 | export { utils }; 17 | export { values }; 18 | export { Parser }; 19 | export { ParserPlugin }; 20 | export { PluginType }; 21 | export { errors }; 22 | export { Ast }; 23 | export { AISCRIPT_VERSION }; 24 | -------------------------------------------------------------------------------- /src/interpreter/control.ts: -------------------------------------------------------------------------------- 1 | import { AiScriptRuntimeError } from '../error.js'; 2 | import { NULL } from './value.js'; 3 | import type { Reference } from './reference.js'; 4 | import type { Value } from './value.js'; 5 | 6 | export type CReturn = { 7 | type: 'return'; 8 | value: Value; 9 | }; 10 | 11 | export type CBreak = { 12 | type: 'break'; 13 | label?: string; 14 | value?: Value; 15 | }; 16 | 17 | export type CContinue = { 18 | type: 'continue'; 19 | label?: string; 20 | value: null; 21 | }; 22 | 23 | export type Control = CReturn | CBreak | CContinue; 24 | 25 | // Return文で値が返されたことを示すためのラッパー 26 | export const RETURN = (v: CReturn['value']): CReturn => ({ 27 | type: 'return' as const, 28 | value: v, 29 | }); 30 | 31 | export const BREAK = (label?: string, value?: CBreak['value']): CBreak => ({ 32 | type: 'break' as const, 33 | label, 34 | value: value, 35 | }); 36 | 37 | export const CONTINUE = (label?: string): CContinue => ({ 38 | type: 'continue' as const, 39 | label, 40 | value: null, 41 | }); 42 | 43 | /** 44 | * 値がbreakで、ラベルが一致する場合のみ、その中身を取り出します。 45 | */ 46 | export function unWrapLabeledBreak(v: Value | Control, label: string | undefined): Value | Control { 47 | if (v.type === 'break' && v.label != null && v.label === label) { 48 | return v.value ?? NULL; 49 | } 50 | return v; 51 | } 52 | 53 | export function unWrapRet(v: Value | Control): Value { 54 | switch (v.type) { 55 | case 'return': 56 | return v.value; 57 | default: { 58 | assertValue(v); 59 | return v; 60 | } 61 | } 62 | } 63 | 64 | export function assertValue(v: Value | Control): asserts v is Value { 65 | switch (v.type) { 66 | case 'return': 67 | throw new AiScriptRuntimeError('Invalid return'); 68 | case 'break': 69 | throw new AiScriptRuntimeError('Invalid break'); 70 | case 'continue': 71 | throw new AiScriptRuntimeError('Invalid continue'); 72 | default: 73 | v satisfies Value; 74 | } 75 | } 76 | 77 | export function isControl(v: Value | Control | Reference): v is Control { 78 | switch (v.type) { 79 | case 'null': 80 | case 'bool': 81 | case 'num': 82 | case 'str': 83 | case 'arr': 84 | case 'obj': 85 | case 'fn': 86 | case 'error': 87 | case 'reference': 88 | return false; 89 | case 'return': 90 | case 'break': 91 | case 'continue': 92 | return true; 93 | } 94 | // exhaustive check 95 | v satisfies never; 96 | throw new TypeError('expected value or control'); 97 | } 98 | -------------------------------------------------------------------------------- /src/interpreter/reference.ts: -------------------------------------------------------------------------------- 1 | import { AiScriptIndexOutOfRangeError } from '../error.js'; 2 | import { assertArray, assertObject } from './util.js'; 3 | import { ARR, NULL, OBJ } from './value.js'; 4 | import type { VArr, VObj, Value } from './value.js'; 5 | import type { Scope } from './scope.js'; 6 | 7 | export interface Reference { 8 | type: 'reference'; 9 | 10 | get(): Value; 11 | 12 | set(value: Value): void; 13 | } 14 | 15 | export const Reference = { 16 | variable(name: string, scope: Scope): Reference { 17 | return new VariableReference(name, scope); 18 | }, 19 | 20 | index(target: VArr, index: number): Reference { 21 | return new IndexReference(target.value, index); 22 | }, 23 | 24 | prop(target: VObj, name: string): Reference { 25 | return new PropReference(target.value, name); 26 | }, 27 | 28 | arr(dest: readonly Reference[]): Reference { 29 | return new ArrReference(dest); 30 | }, 31 | 32 | obj(dest: ReadonlyMap): Reference { 33 | return new ObjReference(dest); 34 | }, 35 | }; 36 | 37 | class VariableReference implements Reference { 38 | constructor(private name: string, private scope: Scope) { 39 | this.type = 'reference'; 40 | } 41 | 42 | type: 'reference'; 43 | 44 | get(): Value { 45 | return this.scope.get(this.name); 46 | } 47 | 48 | set(value: Value): void { 49 | this.scope.assign(this.name, value); 50 | } 51 | } 52 | 53 | class IndexReference implements Reference { 54 | constructor(private target: Value[], private index: number) { 55 | this.type = 'reference'; 56 | } 57 | 58 | type: 'reference'; 59 | 60 | get(): Value { 61 | this.assertIndexInRange(); 62 | return this.target[this.index]!; 63 | } 64 | 65 | set(value: Value): void { 66 | this.assertIndexInRange(); 67 | this.target[this.index] = value; 68 | } 69 | 70 | private assertIndexInRange(): void { 71 | const index = this.index; 72 | if (index < 0 || this.target.length <= index) { 73 | throw new AiScriptIndexOutOfRangeError(`Index out of range. index: ${this.index} max: ${this.target.length - 1}`); 74 | } 75 | } 76 | } 77 | 78 | class PropReference implements Reference { 79 | constructor(private target: Map, private index: string) { 80 | this.type = 'reference'; 81 | } 82 | 83 | type: 'reference'; 84 | 85 | get(): Value { 86 | return this.target.get(this.index) ?? NULL; 87 | } 88 | 89 | set(value: Value): void { 90 | this.target.set(this.index, value); 91 | } 92 | } 93 | 94 | class ArrReference implements Reference { 95 | constructor(private items: readonly Reference[]) { 96 | this.type = 'reference'; 97 | } 98 | 99 | type: 'reference'; 100 | 101 | get(): Value { 102 | return ARR(this.items.map((item) => item.get())); 103 | } 104 | 105 | set(value: Value): void { 106 | assertArray(value); 107 | for (const [index, item] of this.items.entries()) { 108 | item.set(value.value[index] ?? NULL); 109 | } 110 | } 111 | } 112 | 113 | class ObjReference implements Reference { 114 | constructor(private entries: ReadonlyMap) { 115 | this.type = 'reference'; 116 | } 117 | 118 | type: 'reference'; 119 | 120 | get(): Value { 121 | return OBJ(new Map([...this.entries].map(([key, item]) => [key, item.get()]))); 122 | } 123 | 124 | set(value: Value): void { 125 | assertObject(value); 126 | for (const [key, item] of this.entries.entries()) { 127 | item.set(value.value.get(key) ?? NULL); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/interpreter/scope.ts: -------------------------------------------------------------------------------- 1 | import { autobind } from '../utils/mini-autobind.js'; 2 | import { AiScriptRuntimeError } from '../error.js'; 3 | import type { Value } from './value.js'; 4 | import type { Variable } from './variable.js'; 5 | import type { LogObject } from './index.js'; 6 | 7 | export class Scope { 8 | private parent?: Scope; 9 | private layerdStates: Map[]; 10 | public name: string; 11 | public opts: { 12 | log?(type: string, params: LogObject): void; 13 | onUpdated?(name: string, value: Value): void; 14 | } = {}; 15 | public nsName?: string; 16 | 17 | constructor(layerdStates: Scope['layerdStates'] = [], parent?: Scope, name?: Scope['name'], nsName?: string) { 18 | this.layerdStates = layerdStates; 19 | this.parent = parent; 20 | this.name = name || (layerdStates.length === 1 ? '' : ''); 21 | this.nsName = nsName; 22 | } 23 | 24 | @autobind 25 | private log(type: string, params: LogObject): void { 26 | if (this.parent) { 27 | this.parent.log(type, params); 28 | } else { 29 | if (this.opts.log) this.opts.log(type, params); 30 | } 31 | } 32 | 33 | @autobind 34 | private onUpdated(name: string, value: Value): void { 35 | if (this.parent) { 36 | this.parent.onUpdated(name, value); 37 | } else { 38 | if (this.opts.onUpdated) this.opts.onUpdated(name, value); 39 | } 40 | } 41 | 42 | @autobind 43 | public createChildScope(states: Map = new Map(), name?: Scope['name']): Scope { 44 | const layer = [states, ...this.layerdStates]; 45 | return new Scope(layer, this, name); 46 | } 47 | 48 | @autobind 49 | public createChildNamespaceScope(nsName: string, states: Map = new Map(), name?: Scope['name']): Scope { 50 | const layer = [states, ...this.layerdStates]; 51 | return new Scope(layer, this, name, nsName); 52 | } 53 | 54 | /** 55 | * 指定した名前の変数を取得します 56 | * @param name - 変数名 57 | */ 58 | @autobind 59 | public get(name: string): Value { 60 | for (const layer of this.layerdStates) { 61 | if (layer.has(name)) { 62 | const state = layer.get(name)!.value; 63 | this.log('read', { var: name, val: state }); 64 | return state; 65 | } 66 | } 67 | 68 | throw new AiScriptRuntimeError( 69 | `No such variable '${name}' in scope '${this.name}'`, 70 | { scope: this.layerdStates }); 71 | } 72 | 73 | /** 74 | * 名前空間名を取得します。 75 | */ 76 | @autobind 77 | public getNsPrefix(): string { 78 | if (this.parent == null || this.nsName == null) return ''; 79 | return this.parent.getNsPrefix() + this.nsName + ':'; 80 | } 81 | 82 | /** 83 | * 指定した名前の変数が存在するか判定します 84 | * @param name - 変数名 85 | */ 86 | @autobind 87 | public exists(name: string): boolean { 88 | for (const layer of this.layerdStates) { 89 | if (layer.has(name)) { 90 | this.log('exists', { var: name }); 91 | return true; 92 | } 93 | } 94 | 95 | this.log('not exists', { var: name }); 96 | return false; 97 | } 98 | 99 | /** 100 | * 現在のスコープに存在する全ての変数を取得します 101 | */ 102 | @autobind 103 | public getAll(): Map { 104 | const vars = this.layerdStates.reduce((arr, layer) => { 105 | return [...arr, ...layer]; 106 | }, [] as [string, Variable][]); 107 | return new Map(vars); 108 | } 109 | 110 | /** 111 | * 指定した名前の変数を現在のスコープに追加します。名前空間である場合は接頭辞を付して親のスコープにも追加します 112 | * @param name - 変数名 113 | * @param val - 初期値 114 | */ 115 | @autobind 116 | public add(name: string, variable: Variable): void { 117 | this.log('add', { var: name, val: variable }); 118 | const states = this.layerdStates[0]!; 119 | if (states.has(name)) { 120 | throw new AiScriptRuntimeError( 121 | `Variable '${name}' already exists in scope '${this.name}'`, 122 | { scope: this.layerdStates }); 123 | } 124 | states.set(name, variable); 125 | if (this.parent == null) this.onUpdated(name, variable.value); 126 | else if (this.nsName != null) this.parent.add(this.nsName + ':' + name, variable); 127 | } 128 | 129 | /** 130 | * 指定した名前の変数に値を再代入します 131 | * @param name - 変数名 132 | * @param val - 値 133 | */ 134 | @autobind 135 | public assign(name: string, val: Value): void { 136 | let i = 1; 137 | for (const layer of this.layerdStates) { 138 | if (layer.has(name)) { 139 | const variable = layer.get(name)!; 140 | if (!variable.isMutable) { 141 | throw new AiScriptRuntimeError(`Cannot assign to an immutable variable ${name}.`); 142 | } 143 | 144 | variable.value = val; 145 | 146 | this.log('assign', { var: name, val: val }); 147 | if (i === this.layerdStates.length) this.onUpdated(name, val); 148 | return; 149 | } 150 | i++; 151 | } 152 | 153 | throw new AiScriptRuntimeError( 154 | `No such variable '${name}' in scope '${this.name}'`, 155 | { scope: this.layerdStates }); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/interpreter/util.ts: -------------------------------------------------------------------------------- 1 | import { AiScriptRuntimeError } from '../error.js'; 2 | import { STR, NUM, ARR, OBJ, NULL, BOOL } from './value.js'; 3 | import type { Value, VStr, VNum, VBool, VFn, VObj, VArr } from './value.js'; 4 | 5 | export function expectAny(val: Value | null | undefined): asserts val is Value { 6 | if (val == null) { 7 | throw new AiScriptRuntimeError('Expect anything, but got nothing.'); 8 | } 9 | } 10 | 11 | export function assertBoolean(val: Value | null | undefined): asserts val is VBool { 12 | if (val == null) { 13 | throw new AiScriptRuntimeError('Expect boolean, but got nothing.'); 14 | } 15 | if (val.type !== 'bool') { 16 | throw new AiScriptRuntimeError(`Expect boolean, but got ${val.type}.`); 17 | } 18 | } 19 | 20 | export function assertFunction(val: Value | null | undefined): asserts val is VFn { 21 | if (val == null) { 22 | throw new AiScriptRuntimeError('Expect function, but got nothing.'); 23 | } 24 | if (val.type !== 'fn') { 25 | throw new AiScriptRuntimeError(`Expect function, but got ${val.type}.`); 26 | } 27 | } 28 | 29 | export function assertString(val: Value | null | undefined): asserts val is VStr { 30 | if (val == null) { 31 | throw new AiScriptRuntimeError('Expect string, but got nothing.'); 32 | } 33 | if (val.type !== 'str') { 34 | throw new AiScriptRuntimeError(`Expect string, but got ${val.type}.`); 35 | } 36 | } 37 | 38 | export function assertNumber(val: Value | null | undefined): asserts val is VNum { 39 | if (val == null) { 40 | throw new AiScriptRuntimeError('Expect number, but got nothing.'); 41 | } 42 | if (val.type !== 'num') { 43 | throw new AiScriptRuntimeError(`Expect number, but got ${val.type}.`); 44 | } 45 | } 46 | 47 | export function assertObject(val: Value | null | undefined): asserts val is VObj { 48 | if (val == null) { 49 | throw new AiScriptRuntimeError('Expect object, but got nothing.'); 50 | } 51 | if (val.type !== 'obj') { 52 | throw new AiScriptRuntimeError(`Expect object, but got ${val.type}.`); 53 | } 54 | } 55 | 56 | export function assertArray(val: Value | null | undefined): asserts val is VArr { 57 | if (val == null) { 58 | throw new AiScriptRuntimeError('Expect array, but got nothing.'); 59 | } 60 | if (val.type !== 'arr') { 61 | throw new AiScriptRuntimeError(`Expect array, but got ${val.type}.`); 62 | } 63 | } 64 | 65 | export function isBoolean(val: Value): val is VBool { 66 | return val.type === 'bool'; 67 | } 68 | 69 | export function isFunction(val: Value): val is VFn { 70 | return val.type === 'fn'; 71 | } 72 | 73 | export function isString(val: Value): val is VStr { 74 | return val.type === 'str'; 75 | } 76 | 77 | export function isNumber(val: Value): val is VNum { 78 | return val.type === 'num'; 79 | } 80 | 81 | export function isObject(val: Value): val is VObj { 82 | return val.type === 'obj'; 83 | } 84 | 85 | export function isArray(val: Value): val is VArr { 86 | return val.type === 'arr'; 87 | } 88 | 89 | export function eq(a: Value, b: Value): boolean { 90 | if (a.type === 'fn' && b.type === 'fn') return a.native && b.native ? a.native === b.native : a === b; 91 | if (a.type === 'fn' || b.type === 'fn') return false; 92 | if (a.type === 'null' && b.type === 'null') return true; 93 | if (a.type === 'null' || b.type === 'null') return false; 94 | return (a.value === b.value); 95 | } 96 | 97 | export function valToString(val: Value, simple = false): string { 98 | if (simple) { 99 | if (val.type === 'num') return val.value.toString(); 100 | if (val.type === 'bool') return val.value ? 'true' : 'false'; 101 | if (val.type === 'str') return `"${val.value}"`; 102 | if (val.type === 'arr') return `[${val.value.map(item => valToString(item, true)).join(', ')}]`; 103 | if (val.type === 'null') return '(null)'; 104 | } 105 | const label = 106 | val.type === 'num' ? val.value : 107 | val.type === 'bool' ? val.value : 108 | val.type === 'str' ? `"${val.value}"` : 109 | val.type === 'fn' ? '...' : 110 | val.type === 'obj' ? '...' : 111 | val.type === 'null' ? '' : 112 | null; 113 | 114 | return `${val.type}<${label}>`; 115 | } 116 | 117 | export type JsValue = { [key: string]: JsValue } | JsValue[] | string | number | boolean | null | undefined; 118 | 119 | export function valToJs(val: Value): JsValue { 120 | switch (val.type) { 121 | case 'fn': return ''; 122 | case 'arr': return val.value.map(item => valToJs(item)); 123 | case 'bool': return val.value; 124 | case 'null': return null; 125 | case 'num': return val.value; 126 | case 'obj': { 127 | const obj: { [key: string]: JsValue } = {}; 128 | for (const [k, v] of val.value.entries()) { 129 | // TODO: keyが__proto__とかじゃないかチェック 130 | obj[k] = valToJs(v); 131 | } 132 | return obj; 133 | } 134 | case 'str': return val.value; 135 | default: throw new Error(`Unrecognized value type: ${val.type}`); 136 | } 137 | } 138 | 139 | export function jsToVal(val: unknown): Value { 140 | if (val === null) return NULL; 141 | if (typeof val === 'boolean') return BOOL(val); 142 | if (typeof val === 'string') return STR(val); 143 | if (typeof val === 'number') return NUM(val); 144 | if (Array.isArray(val)) return ARR(val.map(item => jsToVal(item))); 145 | if (typeof val === 'object') { 146 | const obj: VObj['value'] = new Map(); 147 | for (const [k, v] of Object.entries(val)) { 148 | obj.set(k, jsToVal(v)); 149 | } 150 | return OBJ(obj); 151 | } 152 | return NULL; 153 | } 154 | 155 | export function getLangVersion(input: string): string | null { 156 | const match = /^\s*\/\/\/\s*@\s*([A-Z0-9_.-]+)(?:[\r\n][\s\S]*)?$/i.exec(input); 157 | return (match != null) ? match[1]! : null; 158 | } 159 | 160 | /** 161 | * @param literalLike - `true` なら出力をリテラルに似せる 162 | */ 163 | export function reprValue(value: Value, literalLike = false, processedObjects = new Set()): string { 164 | if ((value.type === 'arr' || value.type === 'obj') && processedObjects.has(value.value)) { 165 | return '...'; 166 | } 167 | 168 | if (literalLike && value.type === 'str') return '"' + value.value.replace(/["\\\r\n]/g, x => `\\${x}`) + '"'; 169 | if (value.type === 'str') return value.value; 170 | if (value.type === 'num') return value.value.toString(); 171 | if (value.type === 'arr') { 172 | processedObjects.add(value.value); 173 | const content = []; 174 | 175 | for (const item of value.value) { 176 | content.push(reprValue(item, true, processedObjects)); 177 | } 178 | 179 | return '[ ' + content.join(', ') + ' ]'; 180 | } 181 | if (value.type === 'obj') { 182 | processedObjects.add(value.value); 183 | const content = []; 184 | 185 | for (const [key, val] of value.value) { 186 | content.push(`${key}: ${reprValue(val, true, processedObjects)}`); 187 | } 188 | 189 | return '{ ' + content.join(', ') + ' }'; 190 | } 191 | if (value.type === 'bool') return value.value.toString(); 192 | if (value.type === 'null') return 'null'; 193 | if (value.type === 'fn') { 194 | if (value.native) { 195 | // そのうちネイティブ関数の引数も表示できるようにしたいが、ホスト向けの破壊的変更を伴うと思われる 196 | return '@( ?? ) { native code }'; 197 | } else { 198 | return `@( ${(value.params.map(v => v.dest.type === 'identifier' ? v.dest.name : '?')).join(', ')} ) { ... }`; 199 | } 200 | } 201 | 202 | return '?'; 203 | } 204 | -------------------------------------------------------------------------------- /src/interpreter/value.ts: -------------------------------------------------------------------------------- 1 | import type { Expression, Node } from '../node.js'; 2 | import type { Type } from '../type.js'; 3 | import type { Scope } from './scope.js'; 4 | 5 | export type VNull = { 6 | type: 'null'; 7 | }; 8 | 9 | export type VBool = { 10 | type: 'bool'; 11 | value: boolean; 12 | }; 13 | 14 | export type VNum = { 15 | type: 'num'; 16 | value: number; 17 | }; 18 | 19 | export type VStr = { 20 | type: 'str'; 21 | value: string; 22 | }; 23 | 24 | export type VArr = { 25 | type: 'arr'; 26 | value: Value[]; 27 | }; 28 | 29 | export type VObj = { 30 | type: 'obj'; 31 | value: Map; 32 | }; 33 | 34 | export type VFn = VUserFn | VNativeFn; 35 | type VFnBase = { 36 | type: 'fn'; 37 | }; 38 | export type VUserFn = VFnBase & { 39 | native?: undefined; // if (vfn.native) で型アサーション出来るように 40 | name?: string; 41 | params: VFnParam[]; 42 | statements: Node[]; 43 | scope: Scope; 44 | }; 45 | export type VFnParam = { 46 | dest: Expression; 47 | type?: Type; 48 | default?: Value; 49 | } 50 | /** 51 | * When your AiScript NATIVE function passes VFn.call to other caller(s) whose error thrown outside the scope, use VFn.topCall instead to keep it under AiScript error control system. 52 | */ 53 | export type VNativeFn = VFnBase & { 54 | native: (args: (Value | undefined)[], opts: { 55 | call: (fn: VFn, args: Value[]) => Promise; 56 | topCall: (fn: VFn, args: Value[]) => Promise; 57 | registerAbortHandler: (handler: () => void) => void; 58 | registerPauseHandler: (handler: () => void) => void; 59 | registerUnpauseHandler: (handler: () => void) => void; 60 | unregisterAbortHandler: (handler: () => void) => void; 61 | unregisterPauseHandler: (handler: () => void) => void; 62 | unregisterUnpauseHandler: (handler: () => void) => void; 63 | }) => Value | Promise | void; 64 | }; 65 | 66 | export type VError = { 67 | type: 'error'; 68 | value: string; 69 | info?: Value; 70 | }; 71 | 72 | export type Attr = { 73 | attr?: { 74 | name: string; 75 | value: Value; 76 | }[]; 77 | }; 78 | 79 | export type Value = (VNull | VBool | VNum | VStr | VArr | VObj | VFn | VError) & Attr; 80 | 81 | export const NULL = { 82 | type: 'null' as const, 83 | }; 84 | 85 | export const TRUE = { 86 | type: 'bool' as const, 87 | value: true, 88 | }; 89 | 90 | export const FALSE = { 91 | type: 'bool' as const, 92 | value: false, 93 | }; 94 | 95 | export const NUM = (num: VNum['value']): VNum => ({ 96 | type: 'num' as const, 97 | value: num, 98 | }); 99 | 100 | export const STR = (str: VStr['value']): VStr => ({ 101 | type: 'str' as const, 102 | value: str, 103 | }); 104 | 105 | export const BOOL = (bool: VBool['value']): VBool => ({ 106 | type: 'bool' as const, 107 | value: bool, 108 | }); 109 | 110 | export const OBJ = (obj: VObj['value']): VObj => ({ 111 | type: 'obj' as const, 112 | value: obj, 113 | }); 114 | 115 | export const ARR = (arr: VArr['value']): VArr => ({ 116 | type: 'arr' as const, 117 | value: arr, 118 | }); 119 | 120 | export const FN = (params: VUserFn['params'], statements: VUserFn['statements'], scope: VUserFn['scope']): VUserFn => ({ 121 | type: 'fn' as const, 122 | params: params, 123 | statements: statements, 124 | scope: scope, 125 | }); 126 | 127 | export const FN_NATIVE = (fn: VNativeFn['native']): VNativeFn => ({ 128 | type: 'fn' as const, 129 | native: fn, 130 | }); 131 | 132 | export const ERROR = (name: string, info?: Value): Value => ({ 133 | type: 'error' as const, 134 | value: name, 135 | info: info, 136 | }); 137 | -------------------------------------------------------------------------------- /src/interpreter/variable.ts: -------------------------------------------------------------------------------- 1 | import type { Value } from './value.js'; 2 | 3 | export type Variable = 4 | | { 5 | isMutable: false 6 | readonly value: Value 7 | } 8 | | { 9 | isMutable: true 10 | value: Value 11 | } 12 | 13 | export const Variable = { 14 | mut(value: Value): Variable { 15 | return { 16 | isMutable: true, 17 | value, 18 | }; 19 | }, 20 | const(value: Value): Variable { 21 | return { 22 | isMutable: false, 23 | value, 24 | }; 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ASTノード 3 | */ 4 | 5 | export type Pos = { 6 | line: number; 7 | column: number; 8 | }; 9 | 10 | export type Loc = { 11 | start: Pos; 12 | end: Pos; 13 | }; 14 | 15 | export type Node = Namespace | Meta | Statement | Expression | TypeSource | Attribute; 16 | 17 | type NodeBase = { 18 | loc: Loc; // コード位置 19 | }; 20 | 21 | export type Namespace = NodeBase & { 22 | type: 'ns'; // 名前空間 23 | name: string; // 空間名 24 | members: (Definition | Namespace)[]; // メンバー 25 | }; 26 | 27 | export type Meta = NodeBase & { 28 | type: 'meta'; // メタデータ定義 29 | name: string | null; // 名 30 | value: Expression; // 値 31 | }; 32 | 33 | // statement 34 | 35 | export type Statement = 36 | Definition | 37 | Return | 38 | Each | 39 | For | 40 | Loop | 41 | Break | 42 | Continue | 43 | Assign | 44 | AddAssign | 45 | SubAssign; 46 | 47 | const statementTypes = [ 48 | 'def', 'return', 'each', 'for', 'loop', 'break', 'continue', 'assign', 'addAssign', 'subAssign', 49 | ]; 50 | export function isStatement(x: Node): x is Statement { 51 | return statementTypes.includes(x.type); 52 | } 53 | 54 | export type Definition = NodeBase & { 55 | type: 'def'; // 変数宣言文 56 | dest: Expression; // 宣言式 57 | varType?: TypeSource; // 変数の型 58 | expr: Expression; // 式 59 | mut: boolean; // ミュータブルか否か 60 | attr: Attribute[]; // 付加された属性 61 | }; 62 | 63 | export type Attribute = NodeBase & { 64 | type: 'attr'; // 属性 65 | name: string; // 属性名 66 | value: Expression; // 値 67 | }; 68 | 69 | export type Return = NodeBase & { 70 | type: 'return'; // return文 71 | expr: Expression; // 式 72 | }; 73 | 74 | export type Each = NodeBase & { 75 | type: 'each'; // each文 76 | label?: string; // ラベル 77 | var: Expression; // イテレータ宣言 78 | items: Expression; // 配列 79 | for: Statement | Expression; // 本体処理 80 | }; 81 | 82 | export type For = NodeBase & { 83 | type: 'for'; // for文 84 | label?: string; // ラベル 85 | var?: string; // イテレータ変数名 86 | from?: Expression; // 開始値 87 | to?: Expression; // 終値 88 | times?: Expression; // 回数 89 | for: Statement | Expression; // 本体処理 90 | }; 91 | 92 | export type Loop = NodeBase & { 93 | type: 'loop'; // loop文 94 | label?: string; // ラベル 95 | statements: (Statement | Expression)[]; // 処理 96 | }; 97 | 98 | export type Break = NodeBase & { 99 | type: 'break'; // break文 100 | label?: string; // ラベル 101 | expr?: Expression; // 式 102 | }; 103 | 104 | export type Continue = NodeBase & { 105 | type: 'continue'; // continue文 106 | label?: string; // ラベル 107 | }; 108 | 109 | export type AddAssign = NodeBase & { 110 | type: 'addAssign'; // 加算代入文 111 | dest: Expression; // 代入先 112 | expr: Expression; // 式 113 | }; 114 | 115 | export type SubAssign = NodeBase & { 116 | type: 'subAssign'; // 減算代入文 117 | dest: Expression; // 代入先 118 | expr: Expression; // 式 119 | }; 120 | 121 | export type Assign = NodeBase & { 122 | type: 'assign'; // 代入文 123 | dest: Expression; // 代入先 124 | expr: Expression; // 式 125 | }; 126 | 127 | // expressions 128 | 129 | export type Expression = 130 | If | 131 | Fn | 132 | Match | 133 | Block | 134 | Exists | 135 | Tmpl | 136 | Str | 137 | Num | 138 | Bool | 139 | Null | 140 | Obj | 141 | Arr | 142 | Plus | 143 | Minus | 144 | Not | 145 | Pow | 146 | Mul | 147 | Div | 148 | Rem | 149 | Add | 150 | Sub | 151 | Lt | 152 | Lteq | 153 | Gt | 154 | Gteq | 155 | Eq | 156 | Neq | 157 | And | 158 | Or | 159 | Identifier | 160 | Call | 161 | Index | 162 | Prop; 163 | 164 | const expressionTypes = [ 165 | 'if', 'fn', 'match', 'block', 'exists', 'tmpl', 'str', 'num', 'bool', 'null', 'obj', 'arr', 166 | 'not', 'pow', 'mul', 'div', 'rem', 'add', 'sub', 'lt', 'lteq', 'gt', 'gteq', 'eq', 'neq', 'and', 'or', 167 | 'identifier', 'call', 'index', 'prop', 168 | ]; 169 | export function isExpression(x: Node): x is Expression { 170 | return expressionTypes.includes(x.type); 171 | } 172 | 173 | export type Plus = NodeBase & { 174 | type: 'plus'; // 正号 175 | expr: Expression; // 式 176 | }; 177 | 178 | export type Minus = NodeBase & { 179 | type: 'minus'; // 負号 180 | expr: Expression; // 式 181 | }; 182 | 183 | export type Not = NodeBase & { 184 | type: 'not'; // 否定 185 | expr: Expression; // 式 186 | }; 187 | 188 | export type Pow = NodeBase & { 189 | type: 'pow'; 190 | left: Expression; 191 | right: Expression; 192 | } 193 | 194 | export type Mul = NodeBase & { 195 | type: 'mul'; 196 | left: Expression; 197 | right: Expression; 198 | } 199 | 200 | export type Div = NodeBase & { 201 | type: 'div'; 202 | left: Expression; 203 | right: Expression; 204 | } 205 | 206 | export type Rem = NodeBase & { 207 | type: 'rem'; 208 | left: Expression; 209 | right: Expression; 210 | } 211 | 212 | export type Add = NodeBase & { 213 | type: 'add'; 214 | left: Expression; 215 | right: Expression; 216 | } 217 | 218 | export type Sub = NodeBase & { 219 | type: 'sub'; 220 | left: Expression; 221 | right: Expression; 222 | } 223 | 224 | export type Lt = NodeBase & { 225 | type: 'lt'; 226 | left: Expression; 227 | right: Expression; 228 | } 229 | 230 | export type Lteq = NodeBase & { 231 | type: 'lteq'; 232 | left: Expression; 233 | right: Expression; 234 | } 235 | 236 | export type Gt = NodeBase & { 237 | type: 'gt'; 238 | left: Expression; 239 | right: Expression; 240 | } 241 | 242 | export type Gteq = NodeBase & { 243 | type: 'gteq'; 244 | left: Expression; 245 | right: Expression; 246 | } 247 | 248 | export type Eq = NodeBase & { 249 | type: 'eq'; 250 | left: Expression; 251 | right: Expression; 252 | } 253 | 254 | export type Neq = NodeBase & { 255 | type: 'neq'; 256 | left: Expression; 257 | right: Expression; 258 | } 259 | 260 | export type And = NodeBase & { 261 | type: 'and'; 262 | left: Expression; 263 | right: Expression; 264 | } 265 | 266 | export type Or = NodeBase & { 267 | type: 'or'; 268 | left: Expression; 269 | right: Expression; 270 | } 271 | 272 | export type If = NodeBase & { 273 | type: 'if'; // if式 274 | label?: string; // ラベル 275 | cond: Expression; // 条件式 276 | then: Statement | Expression; // then節 277 | elseif: { 278 | cond: Expression; // elifの条件式 279 | then: Statement | Expression;// elif節 280 | }[]; 281 | else?: Statement | Expression; // else節 282 | }; 283 | 284 | export type Fn = NodeBase & { 285 | type: 'fn'; // 関数 286 | typeParams: TypeParam[]; // 型パラメータ 287 | params: { 288 | dest: Expression; // 引数名 289 | optional: boolean; 290 | default?: Expression; // 引数の初期値 291 | argType?: TypeSource; // 引数の型 292 | }[]; 293 | retType?: TypeSource; // 戻り値の型 294 | children: (Statement | Expression)[]; // 本体処理 295 | }; 296 | 297 | export type Match = NodeBase & { 298 | type: 'match'; // パターンマッチ 299 | label?: string; // ラベル 300 | about: Expression; // 対象 301 | qs: { 302 | q: Expression; // 条件 303 | a: Statement | Expression; // 結果 304 | }[]; 305 | default?: Statement | Expression; // デフォルト値 306 | }; 307 | 308 | export type Block = NodeBase & { 309 | type: 'block'; // ブロックまたはeval式 310 | label?: string; // ラベル 311 | statements: (Statement | Expression)[]; // 処理 312 | }; 313 | 314 | export type Exists = NodeBase & { 315 | type: 'exists'; // 変数の存在判定 316 | identifier: Identifier; // 変数名 317 | }; 318 | 319 | export type Tmpl = NodeBase & { 320 | type: 'tmpl'; // テンプレート 321 | tmpl: Expression[]; // 処理 322 | }; 323 | 324 | export type Str = NodeBase & { 325 | type: 'str'; // 文字列リテラル 326 | value: string; // 文字列 327 | }; 328 | 329 | export type Num = NodeBase & { 330 | type: 'num'; // 数値リテラル 331 | value: number; // 数値 332 | }; 333 | 334 | export type Bool = NodeBase & { 335 | type: 'bool'; // 真理値リテラル 336 | value: boolean; // 真理値 337 | }; 338 | 339 | export type Null = NodeBase & { 340 | type: 'null'; // nullリテラル 341 | }; 342 | 343 | export type Obj = NodeBase & { 344 | type: 'obj'; // オブジェクト 345 | value: Map; // プロパティ 346 | }; 347 | 348 | export type Arr = NodeBase & { 349 | type: 'arr'; // 配列 350 | value: Expression[]; // アイテム 351 | }; 352 | 353 | export type Identifier = NodeBase & { 354 | type: 'identifier'; // 変数などの識別子 355 | name: string; // 変数名 356 | }; 357 | 358 | export type Call = NodeBase & { 359 | type: 'call'; // 関数呼び出し 360 | target: Expression; // 対象 361 | args: Expression[]; // 引数 362 | }; 363 | 364 | export type Index = NodeBase & { 365 | type: 'index'; // 配列要素アクセス 366 | target: Expression; // 対象 367 | index: Expression; // インデックス 368 | }; 369 | 370 | export type Prop = NodeBase & { 371 | type: 'prop'; // プロパティアクセス 372 | target: Expression; // 対象 373 | name: string; // プロパティ名 374 | }; 375 | 376 | // Type source 377 | 378 | export type TypeSource = NamedTypeSource | FnTypeSource | UnionTypeSource; 379 | 380 | export type NamedTypeSource = NodeBase & { 381 | type: 'namedTypeSource'; // 名前付き型 382 | name: string; // 型名 383 | inner?: TypeSource; // 内側の型 384 | }; 385 | 386 | export type FnTypeSource = NodeBase & { 387 | type: 'fnTypeSource'; // 関数の型 388 | typeParams: TypeParam[]; // 型パラメータ 389 | params: TypeSource[]; // 引数の型 390 | result: TypeSource; // 戻り値の型 391 | }; 392 | 393 | export type UnionTypeSource = NodeBase & { 394 | type: 'unionTypeSource'; // ユニオン型 395 | inners: TypeSource[]; // 含まれる型 396 | }; 397 | 398 | /** 399 | * 型パラメータ 400 | */ 401 | export type TypeParam = { 402 | name: string; // パラメータ名 403 | } 404 | -------------------------------------------------------------------------------- /src/parser/index.ts: -------------------------------------------------------------------------------- 1 | import { Scanner } from './scanner.js'; 2 | import { parseTopLevel } from './syntaxes/toplevel.js'; 3 | 4 | import { validateJumpStatements } from './plugins/validate-jump-statements.js'; 5 | import { validateKeyword } from './plugins/validate-keyword.js'; 6 | import { validateType } from './plugins/validate-type.js'; 7 | 8 | import type * as Ast from '../node.js'; 9 | 10 | export type ParserPlugin = (nodes: Ast.Node[]) => Ast.Node[]; 11 | export type PluginType = 'validate' | 'transform'; 12 | 13 | export class Parser { 14 | private static instance?: Parser; 15 | private plugins: { 16 | validate: ParserPlugin[]; 17 | transform: ParserPlugin[]; 18 | }; 19 | 20 | constructor() { 21 | this.plugins = { 22 | validate: [ 23 | validateKeyword, 24 | validateType, 25 | validateJumpStatements, 26 | ], 27 | transform: [ 28 | ], 29 | }; 30 | } 31 | 32 | public static parse(input: string): Ast.Node[] { 33 | if (Parser.instance == null) { 34 | Parser.instance = new Parser(); 35 | } 36 | return Parser.instance.parse(input); 37 | } 38 | 39 | public addPlugin(type: PluginType, plugin: ParserPlugin): void { 40 | switch (type) { 41 | case 'validate': 42 | this.plugins.validate.push(plugin); 43 | break; 44 | case 'transform': 45 | this.plugins.transform.push(plugin); 46 | break; 47 | default: 48 | throw new Error('unknown plugin type'); 49 | } 50 | } 51 | 52 | public parse(input: string): Ast.Node[] { 53 | let nodes: Ast.Node[]; 54 | 55 | const scanner = new Scanner(input); 56 | nodes = parseTopLevel(scanner); 57 | 58 | // validate the node tree 59 | for (const plugin of this.plugins.validate) { 60 | nodes = plugin(nodes); 61 | } 62 | 63 | // transform the node tree 64 | for (const plugin of this.plugins.transform) { 65 | nodes = plugin(nodes); 66 | } 67 | 68 | return nodes; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/parser/plugins/validate-jump-statements.ts: -------------------------------------------------------------------------------- 1 | import { visitNode } from '../visit.js'; 2 | import { AiScriptSyntaxError } from '../../error.js'; 3 | 4 | import type * as Ast from '../../node.js'; 5 | 6 | function getCorrespondingBlock(ancestors: Ast.Node[], label?: string): Ast.Each | Ast.For | Ast.Loop | Ast.If | Ast.Match | Ast.Block | undefined { 7 | for (let i = ancestors.length - 1; i >= 0; i--) { 8 | const ancestor = ancestors[i]!; 9 | switch (ancestor.type) { 10 | case 'loop': 11 | case 'for': 12 | case 'each': { 13 | if (label != null && label !== ancestor.label) { 14 | continue; 15 | } 16 | return ancestor; 17 | } 18 | case 'if': 19 | case 'match': 20 | case 'block': { 21 | if (label == null || label !== ancestor.label) { 22 | continue; 23 | } 24 | return ancestor; 25 | } 26 | case 'fn': 27 | return; 28 | } 29 | } 30 | return; 31 | } 32 | 33 | function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { 34 | switch (node.type) { 35 | case 'return': { 36 | if (!ancestors.some(({ type }) => type === 'fn')) { 37 | throw new AiScriptSyntaxError('return must be inside function', node.loc.start); 38 | } 39 | break; 40 | } 41 | case 'break': { 42 | const block = getCorrespondingBlock(ancestors, node.label); 43 | if (block == null) { 44 | if (node.label != null) { 45 | throw new AiScriptSyntaxError(`label "${node.label}" is not defined`, node.loc.start); 46 | } 47 | throw new AiScriptSyntaxError('unlabeled break must be inside for / each / while / do-while / loop', node.loc.start); 48 | } 49 | 50 | switch (block.type) { 51 | case 'each': { 52 | if (ancestors.includes(block.items)) { 53 | throw new AiScriptSyntaxError('break corresponding to each is not allowed in the target', node.loc.start); 54 | } 55 | break; 56 | } 57 | case 'for': { 58 | if (block.times != null && ancestors.includes(block.times)) { 59 | throw new AiScriptSyntaxError('break corresponding to for is not allowed in the count', node.loc.start); 60 | } else if (ancestors.some((ancestor) => ancestor === block.from || ancestor === block.to)) { 61 | throw new AiScriptSyntaxError('break corresponding to for is not allowed in the range', node.loc.start); 62 | } 63 | break; 64 | } 65 | case 'if': { 66 | if (ancestors.includes(block.cond) || block.elseif.some(({ cond }) => ancestors.includes(cond))) { 67 | throw new AiScriptSyntaxError('break corresponding to if is not allowed in the condition', node.loc.start); 68 | } 69 | break; 70 | } 71 | case 'match':{ 72 | if (ancestors.includes(block.about)) { 73 | throw new AiScriptSyntaxError('break corresponding to match is not allowed in the target', node.loc.start); 74 | } 75 | if (block.qs.some(({ q }) => ancestors.includes(q))) { 76 | throw new AiScriptSyntaxError('break corresponding to match is not allowed in the pattern', node.loc.start); 77 | } 78 | break; 79 | } 80 | } 81 | 82 | if (node.expr != null) { 83 | switch (block.type) { 84 | case 'if': 85 | case 'match': 86 | case 'block': 87 | break; 88 | default: 89 | throw new AiScriptSyntaxError('break corresponding to statement cannot include value', node.loc.start); 90 | } 91 | } 92 | break; 93 | } 94 | case 'continue': { 95 | const block = getCorrespondingBlock(ancestors, node.label); 96 | if (block == null) { 97 | if (node.label != null) { 98 | throw new AiScriptSyntaxError(`label "${node.label}" is not defined`, node.loc.start); 99 | } 100 | throw new AiScriptSyntaxError('continue must be inside for / each / while / do-while / loop', node.loc.start); 101 | } else { 102 | switch (block.type) { 103 | case 'each': { 104 | if (ancestors.includes(block.items)) { 105 | throw new AiScriptSyntaxError('continue corresponding to each is not allowed in the target', node.loc.start); 106 | } 107 | break; 108 | } 109 | case 'for': { 110 | if (block.times != null && ancestors.includes(block.times)) { 111 | throw new AiScriptSyntaxError('continue corresponding to for is not allowed in the count', node.loc.start); 112 | } else if (ancestors.some((ancestor) => ancestor === block.from || ancestor === block.to)) { 113 | throw new AiScriptSyntaxError('continue corresponding to for is not allowed in the range', node.loc.start); 114 | } 115 | break; 116 | } 117 | case 'if': 118 | throw new AiScriptSyntaxError('cannot use continue for if', node.loc.start); 119 | case 'match': 120 | throw new AiScriptSyntaxError('cannot use continue for match', node.loc.start); 121 | case 'block': 122 | throw new AiScriptSyntaxError('cannot use continue for eval', node.loc.start); 123 | } 124 | } 125 | break; 126 | } 127 | } 128 | return node; 129 | } 130 | 131 | export function validateJumpStatements(nodes: Ast.Node[]): Ast.Node[] { 132 | for (const node of nodes) { 133 | visitNode(node, validateNode); 134 | } 135 | return nodes; 136 | } 137 | -------------------------------------------------------------------------------- /src/parser/plugins/validate-keyword.ts: -------------------------------------------------------------------------------- 1 | import { AiScriptSyntaxError } from '../../error.js'; 2 | import { visitNode } from '../visit.js'; 3 | import type * as Ast from '../../node.js'; 4 | 5 | // 予約語となっている識別子があるかを確認する。 6 | // - キーワードは字句解析の段階でそれぞれのKeywordトークンとなるため除外 7 | // - 文脈キーワードは識別子に利用できるため除外 8 | 9 | const reservedWord = [ 10 | 'as', 11 | 'async', 12 | 'attr', 13 | 'attribute', 14 | 'await', 15 | 'catch', 16 | 'class', 17 | // 'const', 18 | 'component', 19 | 'constructor', 20 | // 'def', 21 | 'dictionary', 22 | 'enum', 23 | 'export', 24 | 'finally', 25 | 'fn', 26 | // 'func', 27 | // 'function', 28 | 'hash', 29 | 'in', 30 | 'interface', 31 | 'out', 32 | 'private', 33 | 'public', 34 | 'ref', 35 | 'static', 36 | 'struct', 37 | 'table', 38 | 'this', 39 | 'throw', 40 | 'trait', 41 | 'try', 42 | 'undefined', 43 | 'use', 44 | 'using', 45 | 'when', 46 | 'yield', 47 | 'import', 48 | 'is', 49 | 'meta', 50 | 'module', 51 | 'namespace', 52 | 'new', 53 | ]; 54 | 55 | function throwReservedWordError(name: string, loc: Ast.Loc): void { 56 | throw new AiScriptSyntaxError(`Reserved word "${name}" cannot be used as variable name.`, loc.start); 57 | } 58 | 59 | function validateDest(node: Ast.Node): Ast.Node { 60 | return visitNode(node, node => { 61 | switch (node.type) { 62 | case 'null': { 63 | throwReservedWordError(node.type, node.loc); 64 | break; 65 | } 66 | case 'bool': { 67 | throwReservedWordError(`${node.value}`, node.loc); 68 | break; 69 | } 70 | case 'identifier': { 71 | if (reservedWord.includes(node.name)) { 72 | throwReservedWordError(node.name, node.loc); 73 | } 74 | break; 75 | } 76 | } 77 | 78 | return node; 79 | }); 80 | } 81 | 82 | function validateTypeParams(node: Ast.Fn | Ast.FnTypeSource): void { 83 | for (const typeParam of node.typeParams) { 84 | if (reservedWord.includes(typeParam.name)) { 85 | throwReservedWordError(typeParam.name, node.loc); 86 | } 87 | } 88 | } 89 | 90 | function validateNode(node: Ast.Node): Ast.Node { 91 | switch (node.type) { 92 | case 'def': { 93 | validateDest(node.dest); 94 | break; 95 | } 96 | case 'ns': 97 | case 'attr': 98 | case 'identifier': 99 | case 'prop': { 100 | if (reservedWord.includes(node.name)) { 101 | throwReservedWordError(node.name, node.loc); 102 | } 103 | break; 104 | } 105 | case 'meta': { 106 | if (node.name != null && reservedWord.includes(node.name)) { 107 | throwReservedWordError(node.name, node.loc); 108 | } 109 | break; 110 | } 111 | case 'each': { 112 | if (node.label != null && reservedWord.includes(node.label)) { 113 | throwReservedWordError(node.label, node.loc); 114 | } 115 | validateDest(node.var); 116 | break; 117 | } 118 | case 'for': { 119 | if (node.label != null && reservedWord.includes(node.label)) { 120 | throwReservedWordError(node.label, node.loc); 121 | } 122 | if (node.var != null && reservedWord.includes(node.var)) { 123 | throwReservedWordError(node.var, node.loc); 124 | } 125 | break; 126 | } 127 | case 'loop': { 128 | if (node.label != null && reservedWord.includes(node.label)) { 129 | throwReservedWordError(node.label, node.loc); 130 | } 131 | break; 132 | } 133 | case 'break': { 134 | if (node.label != null && reservedWord.includes(node.label)) { 135 | throwReservedWordError(node.label, node.loc); 136 | } 137 | break; 138 | } 139 | case 'continue': { 140 | if (node.label != null && reservedWord.includes(node.label)) { 141 | throwReservedWordError(node.label, node.loc); 142 | } 143 | break; 144 | } 145 | case 'fn': { 146 | validateTypeParams(node); 147 | for (const param of node.params) { 148 | validateDest(param.dest); 149 | } 150 | break; 151 | } 152 | case 'obj': { 153 | for (const name of node.value.keys()) { 154 | if (reservedWord.includes(name)) { 155 | throwReservedWordError(name, node.loc); 156 | } 157 | } 158 | break; 159 | } 160 | case 'namedTypeSource': { 161 | if (reservedWord.includes(node.name)) { 162 | throwReservedWordError(node.name, node.loc); 163 | } 164 | break; 165 | } 166 | case 'fnTypeSource': { 167 | validateTypeParams(node); 168 | break; 169 | } 170 | } 171 | 172 | return node; 173 | } 174 | 175 | export function validateKeyword(nodes: Ast.Node[]): Ast.Node[] { 176 | for (const inner of nodes) { 177 | visitNode(inner, validateNode); 178 | } 179 | return nodes; 180 | } 181 | -------------------------------------------------------------------------------- /src/parser/plugins/validate-type.ts: -------------------------------------------------------------------------------- 1 | import { getTypeBySource } from '../../type.js'; 2 | import { visitNode } from '../visit.js'; 3 | import type * as Ast from '../../node.js'; 4 | 5 | function collectTypeParams(node: Ast.Node, ancestors: Ast.Node[]): Ast.TypeParam[] { 6 | const items = []; 7 | if (node.type === 'fn') { 8 | const typeParamNames = new Set(); 9 | for (const typeParam of node.typeParams) { 10 | if (typeParamNames.has(typeParam.name)) { 11 | throw new Error(`type parameter name ${typeParam.name} is duplicate`); 12 | } 13 | typeParamNames.add(typeParam.name); 14 | } 15 | items.push(...node.typeParams); 16 | } 17 | for (let i = ancestors.length - 1; i >= 0; i--) { 18 | const ancestor = ancestors[i]!; 19 | if (ancestor.type === 'fn') { 20 | items.push(...ancestor.typeParams); 21 | } 22 | } 23 | return items; 24 | } 25 | 26 | function validateNode(node: Ast.Node, ancestors: Ast.Node[]): Ast.Node { 27 | switch (node.type) { 28 | case 'def': { 29 | if (node.varType != null) { 30 | getTypeBySource(node.varType, collectTypeParams(node, ancestors)); 31 | } 32 | break; 33 | } 34 | case 'fn': { 35 | for (const param of node.params) { 36 | if (param.argType != null) { 37 | getTypeBySource(param.argType, collectTypeParams(node, ancestors)); 38 | } 39 | } 40 | if (node.retType != null) { 41 | getTypeBySource(node.retType, collectTypeParams(node, ancestors)); 42 | } 43 | break; 44 | } 45 | } 46 | 47 | return node; 48 | } 49 | 50 | export function validateType(nodes: Ast.Node[]): Ast.Node[] { 51 | for (const node of nodes) { 52 | visitNode(node, validateNode); 53 | } 54 | return nodes; 55 | } 56 | -------------------------------------------------------------------------------- /src/parser/streams/char-stream.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 入力文字列から文字を読み取るクラス 3 | */ 4 | export class CharStream { 5 | private pages: Map; 6 | private firstPageIndex: number; 7 | private lastPageIndex: number; 8 | private pageIndex: number; 9 | private address: number; 10 | private _char?: string; 11 | /** zero-based number */ 12 | private line: number; 13 | /** zero-based number */ 14 | private column: number; 15 | 16 | constructor(source: string, opts?: { line?: number, column?: number }) { 17 | this.pages = new Map(); 18 | this.pages.set(0, source); 19 | this.firstPageIndex = 0; 20 | this.lastPageIndex = 0; 21 | this.pageIndex = 0; 22 | this.address = 0; 23 | this.line = opts?.line ?? 0; 24 | this.column = opts?.column ?? 0; 25 | this.moveNext(); 26 | } 27 | 28 | /** 29 | * ストリームの終わりに達しているかどうかを取得します。 30 | */ 31 | public get eof(): boolean { 32 | return this.endOfPage && this.isLastPage; 33 | } 34 | 35 | /** 36 | * カーソル位置にある文字を取得します。 37 | */ 38 | public get char(): string { 39 | if (this.eof) { 40 | throw new Error('end of stream'); 41 | } 42 | return this._char!; 43 | } 44 | 45 | /** 46 | * カーソル位置に対応するソースコード上の行番号と列番号を取得します。 47 | */ 48 | public getPos(): { line: number, column: number } { 49 | return { 50 | line: (this.line + 1), 51 | column: (this.column + 1), 52 | }; 53 | } 54 | 55 | /** 56 | * カーソル位置を次の文字へ進めます。 57 | */ 58 | public next(): void { 59 | if (!this.eof && this._char === '\n') { 60 | this.line++; 61 | this.column = 0; 62 | } else { 63 | this.column++; 64 | } 65 | this.incAddr(); 66 | this.moveNext(); 67 | } 68 | 69 | /** 70 | * カーソル位置を前の文字へ戻します。 71 | */ 72 | public prev(): void { 73 | this.decAddr(); 74 | this.movePrev(); 75 | } 76 | 77 | private get isFirstPage(): boolean { 78 | return (this.pageIndex <= this.firstPageIndex); 79 | } 80 | 81 | private get isLastPage(): boolean { 82 | return (this.pageIndex >= this.lastPageIndex); 83 | } 84 | 85 | private get endOfPage(): boolean { 86 | const page = this.pages.get(this.pageIndex)!; 87 | return (this.address >= page.length); 88 | } 89 | 90 | private moveNext(): void { 91 | this.loadChar(); 92 | while (true) { 93 | if (!this.eof && this._char === '\r') { 94 | this.incAddr(); 95 | this.loadChar(); 96 | continue; 97 | } 98 | break; 99 | } 100 | } 101 | 102 | private incAddr(): void { 103 | if (!this.endOfPage) { 104 | this.address++; 105 | } else if (!this.isLastPage) { 106 | this.pageIndex++; 107 | this.address = 0; 108 | } 109 | } 110 | 111 | private movePrev(): void { 112 | this.loadChar(); 113 | while (true) { 114 | if (!this.eof && this._char === '\r') { 115 | this.decAddr(); 116 | this.loadChar(); 117 | continue; 118 | } 119 | break; 120 | } 121 | } 122 | 123 | private decAddr(): void { 124 | if (this.address > 0) { 125 | this.address--; 126 | } else if (!this.isFirstPage) { 127 | this.pageIndex--; 128 | this.address = this.pages.get(this.pageIndex)!.length - 1; 129 | } 130 | } 131 | 132 | private loadChar(): void { 133 | if (this.eof) { 134 | this._char = undefined; 135 | } else { 136 | this._char = this.pages.get(this.pageIndex)![this.address]!; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/parser/streams/token-stream.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN, TokenKind } from '../token.js'; 2 | import { unexpectedTokenError } from '../utils.js'; 3 | import type { Token, TokenPosition } from '../token.js'; 4 | 5 | /** 6 | * トークンの読み取りに関するインターフェース 7 | */ 8 | export interface ITokenStream { 9 | /** 10 | * カーソル位置にあるトークンを取得します。 11 | */ 12 | getToken(): Token; 13 | 14 | /** 15 | * カーソル位置にあるトークンの種類が指定したトークンの種類と一致するかどうかを示す値を取得します。 16 | */ 17 | is(kind: TokenKind): boolean; 18 | 19 | /** 20 | * カーソル位置にあるトークンの種類を取得します。 21 | */ 22 | getTokenKind(): TokenKind; 23 | 24 | /** 25 | * カーソル位置にあるトークンに含まれる値を取得します。 26 | */ 27 | getTokenValue(): string; 28 | 29 | /** 30 | * カーソル位置にあるトークンの位置情報を取得します。 31 | */ 32 | getPos(): TokenPosition; 33 | 34 | /** 35 | * カーソル位置を次のトークンへ進めます。 36 | */ 37 | next(): void; 38 | 39 | /** 40 | * トークンの先読みを行います。カーソル位置は移動されません。 41 | */ 42 | lookahead(offset: number): Token; 43 | 44 | /** 45 | * カーソル位置にあるトークンの種類が指定したトークンの種類と一致することを確認します。 46 | * 一致しなかった場合には文法エラーを発生させます。 47 | */ 48 | expect(kind: TokenKind): void; 49 | } 50 | 51 | /** 52 | * トークン列からトークンを読み取るクラス 53 | */ 54 | export class TokenStream implements ITokenStream { 55 | private source: Token[]; 56 | private index: number; 57 | private _token: Token; 58 | 59 | constructor(source: TokenStream['source']) { 60 | this.source = source; 61 | this.index = 0; 62 | this.load(); 63 | } 64 | 65 | private get eof(): boolean { 66 | return (this.index >= this.source.length); 67 | } 68 | 69 | /** 70 | * カーソル位置にあるトークンを取得します。 71 | */ 72 | public getToken(): Token { 73 | if (this.eof) { 74 | return TOKEN(TokenKind.EOF, { line: -1, column: -1 }); 75 | } 76 | return this._token; 77 | } 78 | 79 | /** 80 | * カーソル位置にあるトークンの種類が指定したトークンの種類と一致するかどうかを示す値を取得します。 81 | */ 82 | public is(kind: TokenKind): boolean { 83 | return this.getTokenKind() === kind; 84 | } 85 | 86 | /** 87 | * カーソル位置にあるトークンに含まれる値を取得します。 88 | */ 89 | public getTokenValue(): string { 90 | return this.getToken().value!; 91 | } 92 | 93 | /** 94 | * カーソル位置にあるトークンの種類を取得します。 95 | */ 96 | public getTokenKind(): TokenKind { 97 | return this.getToken().kind; 98 | } 99 | 100 | /** 101 | * カーソル位置にあるトークンの位置情報を取得します。 102 | */ 103 | public getPos(): TokenPosition { 104 | return this.getToken().pos; 105 | } 106 | 107 | /** 108 | * カーソル位置を次のトークンへ進めます。 109 | */ 110 | public next(): void { 111 | if (!this.eof) { 112 | this.index++; 113 | } 114 | this.load(); 115 | } 116 | 117 | /** 118 | * トークンの先読みを行います。カーソル位置は移動されません。 119 | */ 120 | public lookahead(offset: number): Token { 121 | if (this.index + offset < this.source.length) { 122 | return this.source[this.index + offset]!; 123 | } else { 124 | return TOKEN(TokenKind.EOF, { line: -1, column: -1 }); 125 | } 126 | } 127 | 128 | /** 129 | * カーソル位置にあるトークンの種類が指定したトークンの種類と一致することを確認します。 130 | * 一致しなかった場合には文法エラーを発生させます。 131 | */ 132 | public expect(kind: TokenKind): void { 133 | if (!this.is(kind)) { 134 | throw unexpectedTokenError(this.getTokenKind(), this.getPos()); 135 | } 136 | } 137 | 138 | private load(): void { 139 | if (this.eof) { 140 | this._token = TOKEN(TokenKind.EOF, { line: -1, column: -1 }); 141 | } else { 142 | this._token = this.source[this.index]!; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/parser/syntaxes/common.ts: -------------------------------------------------------------------------------- 1 | import { TokenKind } from '../token.js'; 2 | import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; 3 | import { NODE } from '../utils.js'; 4 | import { parseStatement } from './statements.js'; 5 | import { parseExpr } from './expressions.js'; 6 | import { parseType } from './types.js'; 7 | 8 | import type { ITokenStream } from '../streams/token-stream.js'; 9 | import type * as Ast from '../../node.js'; 10 | 11 | /** 12 | * ```abnf 13 | * Dest = IDENT / Expr 14 | * ``` 15 | */ 16 | export function parseDest(s: ITokenStream): Ast.Expression { 17 | // 全部parseExprに任せるとparseReferenceが型注釈を巻き込んでパースしてしまうためIdentifierのみ個別に処理。 18 | if (s.is(TokenKind.Identifier)) { 19 | const nameStartPos = s.getPos(); 20 | const name = s.getTokenValue(); 21 | s.next(); 22 | return NODE('identifier', { name }, nameStartPos, s.getPos()); 23 | } else { 24 | return parseExpr(s, false); 25 | } 26 | } 27 | 28 | /** 29 | * ```abnf 30 | * Params = "(" [Dest [":" Type] *(SEP Dest [":" Type])] ")" 31 | * ``` 32 | */ 33 | export function parseParams(s: ITokenStream): Ast.Fn['params'] { 34 | const items: Ast.Fn['params'] = []; 35 | 36 | s.expect(TokenKind.OpenParen); 37 | s.next(); 38 | 39 | if (s.is(TokenKind.NewLine)) { 40 | s.next(); 41 | } 42 | 43 | while (!s.is(TokenKind.CloseParen)) { 44 | const dest = parseDest(s); 45 | 46 | let optional = false; 47 | let defaultExpr: Ast.Expression | undefined; 48 | if (s.is(TokenKind.Question)) { 49 | s.next(); 50 | optional = true; 51 | } else if (s.is(TokenKind.Eq)) { 52 | s.next(); 53 | defaultExpr = parseExpr(s, false); 54 | } 55 | let type: Ast.TypeSource | undefined; 56 | if (s.is(TokenKind.Colon)) { 57 | s.next(); 58 | type = parseType(s); 59 | } 60 | 61 | items.push({ dest, optional, default: defaultExpr, argType: type }); 62 | 63 | // separator 64 | switch (s.getTokenKind()) { 65 | case TokenKind.NewLine: { 66 | s.next(); 67 | break; 68 | } 69 | case TokenKind.Comma: { 70 | s.next(); 71 | if (s.is(TokenKind.NewLine)) { 72 | s.next(); 73 | } 74 | break; 75 | } 76 | case TokenKind.CloseParen: { 77 | break; 78 | } 79 | case TokenKind.EOF: { 80 | throw new AiScriptUnexpectedEOFError(s.getPos()); 81 | } 82 | default: { 83 | throw new AiScriptSyntaxError('separator expected', s.getPos()); 84 | } 85 | } 86 | } 87 | 88 | s.expect(TokenKind.CloseParen); 89 | s.next(); 90 | 91 | return items; 92 | } 93 | 94 | /** 95 | * ```abnf 96 | * Block = "{" *Statement "}" 97 | * ``` 98 | */ 99 | export function parseBlock(s: ITokenStream): (Ast.Statement | Ast.Expression)[] { 100 | s.expect(TokenKind.OpenBrace); 101 | s.next(); 102 | 103 | while (s.is(TokenKind.NewLine)) { 104 | s.next(); 105 | } 106 | 107 | const steps: (Ast.Statement | Ast.Expression)[] = []; 108 | while (!s.is(TokenKind.CloseBrace)) { 109 | steps.push(parseStatement(s)); 110 | 111 | // terminator 112 | switch (s.getTokenKind()) { 113 | case TokenKind.NewLine: 114 | case TokenKind.SemiColon: { 115 | while (s.is(TokenKind.NewLine) || s.is(TokenKind.SemiColon)) { 116 | s.next(); 117 | } 118 | break; 119 | } 120 | case TokenKind.CloseBrace: { 121 | break; 122 | } 123 | case TokenKind.EOF: { 124 | throw new AiScriptUnexpectedEOFError(s.getPos()); 125 | } 126 | default: { 127 | throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos()); 128 | } 129 | } 130 | } 131 | 132 | s.expect(TokenKind.CloseBrace); 133 | s.next(); 134 | 135 | return steps; 136 | } 137 | 138 | /** 139 | * ```abnf 140 | * Label = "#" IDENT 141 | * ``` 142 | */ 143 | export function parseLabel(s: ITokenStream): string { 144 | s.expect(TokenKind.Sharp); 145 | s.next(); 146 | 147 | if (s.getToken().hasLeftSpacing) { 148 | throw new AiScriptSyntaxError('cannot use spaces in a label', s.getPos()); 149 | } 150 | s.expect(TokenKind.Identifier); 151 | const label = s.getTokenValue(); 152 | s.next(); 153 | 154 | return label; 155 | } 156 | 157 | /** 158 | * ```abnf 159 | * OptionalSeparator = [SEP] 160 | * ``` 161 | */ 162 | export function parseOptionalSeparator(s: ITokenStream): boolean { 163 | switch (s.getTokenKind()) { 164 | case TokenKind.NewLine: { 165 | s.next(); 166 | return true; 167 | } 168 | case TokenKind.Comma: { 169 | s.next(); 170 | if (s.is(TokenKind.NewLine)) { 171 | s.next(); 172 | } 173 | return true; 174 | } 175 | default: { 176 | return false; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/parser/syntaxes/toplevel.ts: -------------------------------------------------------------------------------- 1 | import { NODE } from '../utils.js'; 2 | import { TokenKind } from '../token.js'; 3 | import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; 4 | import { parseDefStatement, parseStatement, parseStatementWithAttr } from './statements.js'; 5 | import { parseExpr } from './expressions.js'; 6 | 7 | import type * as Ast from '../../node.js'; 8 | import type { ITokenStream } from '../streams/token-stream.js'; 9 | 10 | /** 11 | * ```abnf 12 | * TopLevel = *(Namespace / Meta / Statement) 13 | * ``` 14 | */ 15 | export function parseTopLevel(s: ITokenStream): Ast.Node[] { 16 | const nodes: Ast.Node[] = []; 17 | 18 | while (s.is(TokenKind.NewLine)) { 19 | s.next(); 20 | } 21 | 22 | while (!s.is(TokenKind.EOF)) { 23 | switch (s.getTokenKind()) { 24 | case TokenKind.Colon2: { 25 | nodes.push(parseNamespace(s)); 26 | break; 27 | } 28 | case TokenKind.Sharp3: { 29 | nodes.push(parseMeta(s)); 30 | break; 31 | } 32 | default: { 33 | nodes.push(parseStatement(s)); 34 | break; 35 | } 36 | } 37 | 38 | // terminator 39 | switch (s.getTokenKind()) { 40 | case TokenKind.NewLine: 41 | case TokenKind.SemiColon: { 42 | while (s.is(TokenKind.NewLine) || s.is(TokenKind.SemiColon)) { 43 | s.next(); 44 | } 45 | break; 46 | } 47 | case TokenKind.EOF: { 48 | break; 49 | } 50 | default: { 51 | throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos()); 52 | } 53 | } 54 | } 55 | 56 | return nodes; 57 | } 58 | 59 | /** 60 | * ```abnf 61 | * Namespace = "::" IDENT "{" *(VarDef / FnDef / Namespace) "}" 62 | * ``` 63 | */ 64 | export function parseNamespace(s: ITokenStream): Ast.Namespace { 65 | const startPos = s.getPos(); 66 | 67 | s.expect(TokenKind.Colon2); 68 | s.next(); 69 | 70 | s.expect(TokenKind.Identifier); 71 | const name = s.getTokenValue(); 72 | s.next(); 73 | 74 | const members: (Ast.Namespace | Ast.Definition)[] = []; 75 | s.expect(TokenKind.OpenBrace); 76 | s.next(); 77 | 78 | while (s.is(TokenKind.NewLine)) { 79 | s.next(); 80 | } 81 | 82 | while (!s.is(TokenKind.CloseBrace)) { 83 | switch (s.getTokenKind()) { 84 | case TokenKind.VarKeyword: 85 | case TokenKind.LetKeyword: 86 | case TokenKind.At: { 87 | members.push(parseDefStatement(s)); 88 | break; 89 | } 90 | case TokenKind.Colon2: { 91 | members.push(parseNamespace(s)); 92 | break; 93 | } 94 | case TokenKind.OpenSharpBracket: { 95 | members.push(parseStatementWithAttr(s)); 96 | break; 97 | } 98 | } 99 | 100 | // terminator 101 | switch (s.getTokenKind()) { 102 | case TokenKind.NewLine: 103 | case TokenKind.SemiColon: { 104 | while (s.is(TokenKind.NewLine) || s.is(TokenKind.SemiColon)) { 105 | s.next(); 106 | } 107 | break; 108 | } 109 | case TokenKind.CloseBrace: { 110 | break; 111 | } 112 | case TokenKind.EOF: { 113 | throw new AiScriptUnexpectedEOFError(s.getPos()); 114 | } 115 | default: { 116 | throw new AiScriptSyntaxError('Multiple statements cannot be placed on a single line.', s.getPos()); 117 | } 118 | } 119 | } 120 | s.expect(TokenKind.CloseBrace); 121 | s.next(); 122 | 123 | return NODE('ns', { name, members }, startPos, s.getPos()); 124 | } 125 | 126 | /** 127 | * ```abnf 128 | * Meta = "###" [IDENT] StaticExpr 129 | * ``` 130 | */ 131 | export function parseMeta(s: ITokenStream): Ast.Meta { 132 | const startPos = s.getPos(); 133 | 134 | s.expect(TokenKind.Sharp3); 135 | s.next(); 136 | 137 | let name = null; 138 | if (s.is(TokenKind.Identifier)) { 139 | name = s.getTokenValue(); 140 | s.next(); 141 | } 142 | 143 | const value = parseExpr(s, true); 144 | 145 | return NODE('meta', { name, value }, startPos, value.loc.end); 146 | } 147 | -------------------------------------------------------------------------------- /src/parser/syntaxes/types.ts: -------------------------------------------------------------------------------- 1 | import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../../error.js'; 2 | import { TokenKind } from '../token.js'; 3 | import { NODE } from '../utils.js'; 4 | import { parseOptionalSeparator } from './common.js'; 5 | 6 | import type { Ast } from '../../index.js'; 7 | import type { ITokenStream } from '../streams/token-stream.js'; 8 | import type { TypeParam } from '../../node.js'; 9 | 10 | /** 11 | * ```abnf 12 | * Type = FnType / NamedType 13 | * ``` 14 | */ 15 | export function parseType(s: ITokenStream): Ast.TypeSource { 16 | return parseUnionType(s); 17 | } 18 | 19 | /** 20 | * ```abnf 21 | * TypeParams = "<" TypeParam *(SEP TypeParam) [SEP] ">" 22 | * ``` 23 | */ 24 | export function parseTypeParams(s: ITokenStream): TypeParam[] { 25 | s.expect(TokenKind.Lt); 26 | s.next(); 27 | 28 | if (s.is(TokenKind.NewLine)) { 29 | s.next(); 30 | } 31 | 32 | const items: TypeParam[] = [parseTypeParam(s)]; 33 | 34 | while (parseOptionalSeparator(s)) { 35 | if (s.is(TokenKind.Gt)) { 36 | break; 37 | } 38 | const item = parseTypeParam(s); 39 | items.push(item); 40 | } 41 | 42 | s.expect(TokenKind.Gt); 43 | s.next(); 44 | 45 | return items; 46 | } 47 | 48 | /** 49 | * ```abnf 50 | * TypeParam = IDENT 51 | * ``` 52 | */ 53 | function parseTypeParam(s: ITokenStream): TypeParam { 54 | s.expect(TokenKind.Identifier); 55 | const name = s.getTokenValue(); 56 | s.next(); 57 | 58 | return { name }; 59 | } 60 | 61 | /** 62 | * ```abnf 63 | * UnionType = UnionTypeInner *("|" UnionTypeInner) 64 | * ``` 65 | */ 66 | function parseUnionType(s: ITokenStream): Ast.TypeSource { 67 | const startPos = s.getPos(); 68 | 69 | const first = parseUnionTypeInner(s); 70 | if (!s.is(TokenKind.Or)) { 71 | return first; 72 | } 73 | 74 | const inners = [first]; 75 | do { 76 | s.next(); 77 | inners.push(parseUnionTypeInner(s)); 78 | } while (s.is(TokenKind.Or)); 79 | 80 | return NODE('unionTypeSource', { inners }, startPos, s.getPos()); 81 | } 82 | 83 | /** 84 | * ```abnf 85 | * UnionTypeTerm = FnType / NamedType 86 | * ``` 87 | */ 88 | function parseUnionTypeInner(s: ITokenStream): Ast.TypeSource { 89 | if (s.is(TokenKind.At)) { 90 | return parseFnType(s); 91 | } else { 92 | return parseNamedType(s); 93 | } 94 | } 95 | 96 | /** 97 | * ```abnf 98 | * FnType = "@" [TypeParams] "(" ParamTypes ")" "=>" Type 99 | * ParamTypes = [Type *(SEP Type)] 100 | * ``` 101 | */ 102 | function parseFnType(s: ITokenStream): Ast.TypeSource { 103 | const startPos = s.getPos(); 104 | 105 | s.expect(TokenKind.At); 106 | s.next(); 107 | 108 | let typeParams: Ast.TypeParam[]; 109 | if (s.is(TokenKind.Lt)) { 110 | typeParams = parseTypeParams(s); 111 | } else { 112 | typeParams = []; 113 | } 114 | 115 | s.expect(TokenKind.OpenParen); 116 | s.next(); 117 | 118 | const params: Ast.TypeSource[] = []; 119 | while (!s.is(TokenKind.CloseParen)) { 120 | if (params.length > 0) { 121 | switch (s.getTokenKind()) { 122 | case TokenKind.Comma: { 123 | s.next(); 124 | break; 125 | } 126 | case TokenKind.EOF: { 127 | throw new AiScriptUnexpectedEOFError(s.getPos()); 128 | } 129 | default: { 130 | throw new AiScriptSyntaxError('separator expected', s.getPos()); 131 | } 132 | } 133 | } 134 | const type = parseType(s); 135 | params.push(type); 136 | } 137 | 138 | s.expect(TokenKind.CloseParen); 139 | s.next(); 140 | s.expect(TokenKind.Arrow); 141 | s.next(); 142 | 143 | const resultType = parseType(s); 144 | 145 | return NODE('fnTypeSource', { typeParams, params, result: resultType }, startPos, s.getPos()); 146 | } 147 | 148 | /** 149 | * ```abnf 150 | * NamedType = IDENT ["<" Type ">"] 151 | * ``` 152 | */ 153 | function parseNamedType(s: ITokenStream): Ast.TypeSource { 154 | const startPos = s.getPos(); 155 | 156 | let name: string; 157 | if (s.is(TokenKind.Identifier)) { 158 | name = s.getTokenValue(); 159 | s.next(); 160 | } else { 161 | s.expect(TokenKind.NullKeyword); 162 | s.next(); 163 | name = 'null'; 164 | } 165 | 166 | // inner type 167 | let inner: Ast.TypeSource | undefined; 168 | if (s.is(TokenKind.Lt)) { 169 | s.next(); 170 | inner = parseType(s); 171 | s.expect(TokenKind.Gt); 172 | s.next(); 173 | } 174 | 175 | return NODE('namedTypeSource', { name, inner }, startPos, s.getPos()); 176 | } 177 | -------------------------------------------------------------------------------- /src/parser/token.ts: -------------------------------------------------------------------------------- 1 | export enum TokenKind { 2 | EOF, 3 | NewLine, 4 | Identifier, 5 | 6 | // literal 7 | NumberLiteral, 8 | StringLiteral, 9 | 10 | // template string 11 | Template, 12 | TemplateStringElement, 13 | TemplateExprElement, 14 | 15 | // keyword 16 | NullKeyword, 17 | TrueKeyword, 18 | FalseKeyword, 19 | EachKeyword, 20 | ForKeyword, 21 | LoopKeyword, 22 | DoKeyword, 23 | WhileKeyword, 24 | BreakKeyword, 25 | ContinueKeyword, 26 | MatchKeyword, 27 | CaseKeyword, 28 | DefaultKeyword, 29 | IfKeyword, 30 | ElifKeyword, 31 | ElseKeyword, 32 | ReturnKeyword, 33 | EvalKeyword, 34 | VarKeyword, 35 | LetKeyword, 36 | ExistsKeyword, 37 | 38 | /** "!" */ 39 | Not, 40 | /** "!=" */ 41 | NotEq, 42 | /** "#" */ 43 | Sharp, 44 | /** "#[" */ 45 | OpenSharpBracket, 46 | /** "###" */ 47 | Sharp3, 48 | /** "%" */ 49 | Percent, 50 | /** "&&" */ 51 | And2, 52 | /** "(" */ 53 | OpenParen, 54 | /** ")" */ 55 | CloseParen, 56 | /** "*" */ 57 | Asterisk, 58 | /** "+" */ 59 | Plus, 60 | /** "+=" */ 61 | PlusEq, 62 | /** "," */ 63 | Comma, 64 | /** "-" */ 65 | Minus, 66 | /** "-=" */ 67 | MinusEq, 68 | /** "." */ 69 | Dot, 70 | /** "/" */ 71 | Slash, 72 | /** ":" */ 73 | Colon, 74 | /** "::" */ 75 | Colon2, 76 | /** ";" */ 77 | SemiColon, 78 | /** "<" */ 79 | Lt, 80 | /** "<=" */ 81 | LtEq, 82 | /** "<:" */ 83 | Out, 84 | /** "=" */ 85 | Eq, 86 | /** "==" */ 87 | Eq2, 88 | /** "=>" */ 89 | Arrow, 90 | /** ">" */ 91 | Gt, 92 | /** ">=" */ 93 | GtEq, 94 | /** "?" */ 95 | Question, 96 | /** "@" */ 97 | At, 98 | /** "[" */ 99 | OpenBracket, 100 | /** "\\" */ 101 | BackSlash, 102 | /** "]" */ 103 | CloseBracket, 104 | /** "^" */ 105 | Hat, 106 | /** "{" */ 107 | OpenBrace, 108 | /** "|" */ 109 | Or, 110 | /** "||" */ 111 | Or2, 112 | /** "}" */ 113 | CloseBrace, 114 | } 115 | 116 | export type TokenPosition = { column: number, line: number }; 117 | 118 | export class Token { 119 | constructor( 120 | public kind: TokenKind, 121 | public pos: TokenPosition, 122 | public hasLeftSpacing = false, 123 | /** for number literal, string literal */ 124 | public value?: string, 125 | /** for template syntax */ 126 | public children?: Token[], 127 | ) { } 128 | } 129 | 130 | /** 131 | * - opts.value: for number literal, string literal 132 | * - opts.children: for template syntax 133 | */ 134 | export function TOKEN(kind: TokenKind, pos: TokenPosition, opts?: { hasLeftSpacing?: boolean, value?: Token['value'], children?: Token['children'] }): Token { 135 | return new Token(kind, pos, opts?.hasLeftSpacing, opts?.value, opts?.children); 136 | } 137 | -------------------------------------------------------------------------------- /src/parser/utils.ts: -------------------------------------------------------------------------------- 1 | import { AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../error.js'; 2 | import { TokenKind } from './token.js'; 3 | import type { AiScriptError } from '../error.js'; 4 | import type * as Ast from '../node.js'; 5 | 6 | export function NODE( 7 | type: T, 8 | params: Omit, 'type' | 'loc'>, 9 | start: Ast.Pos, 10 | end: Ast.Pos, 11 | ): Extract { 12 | const node: Record = { type }; 13 | for (const key of Object.keys(params)) { 14 | type Key = keyof typeof params; 15 | if (params[key as Key] !== undefined) { 16 | node[key] = params[key as Key]; 17 | } 18 | } 19 | node.loc = { start, end }; 20 | return node as Extract; 21 | } 22 | 23 | export function CALL_NODE( 24 | name: string, 25 | args: Ast.Expression[], 26 | start: Ast.Pos, 27 | end: Ast.Pos, 28 | ): Ast.Call { 29 | return NODE('call', { 30 | // 糖衣構文はidentifierがソースコードに出現しないので長さ0とする。 31 | target: NODE('identifier', { name }, start, start), 32 | args, 33 | }, start, end); 34 | } 35 | 36 | export function unexpectedTokenError(token: TokenKind, pos: Ast.Pos, info?: unknown): AiScriptError { 37 | if (token === TokenKind.EOF) { 38 | return new AiScriptUnexpectedEOFError(pos, info); 39 | } else { 40 | return new AiScriptSyntaxError(`unexpected token: ${TokenKind[token]}`, pos, info); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/parser/visit.ts: -------------------------------------------------------------------------------- 1 | import type * as Ast from '../node.js'; 2 | 3 | export function visitNode(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node[]) => Ast.Node): Ast.Node { 4 | return visitNodeInner(node, fn, []); 5 | } 6 | 7 | function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node[]) => Ast.Node, ancestors: Ast.Node[]): Ast.Node { 8 | const result = fn(node, ancestors); 9 | ancestors.push(node); 10 | 11 | // nested nodes 12 | switch (result.type) { 13 | case 'def': { 14 | if (result.varType != null) { 15 | result.varType = visitNodeInner(result.varType, fn, ancestors) as Ast.Definition['varType']; 16 | } 17 | result.attr = result.attr.map((attr) => visitNodeInner(attr, fn, ancestors) as Ast.Attribute); 18 | result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Definition['expr']; 19 | break; 20 | } 21 | case 'return': { 22 | result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr']; 23 | break; 24 | } 25 | case 'each': { 26 | result.items = visitNodeInner(result.items, fn, ancestors) as Ast.Each['items']; 27 | result.for = visitNodeInner(result.for, fn, ancestors) as Ast.Each['for']; 28 | break; 29 | } 30 | case 'for': { 31 | if (result.from != null) { 32 | result.from = visitNodeInner(result.from, fn, ancestors) as Ast.For['from']; 33 | } 34 | if (result.to != null) { 35 | result.to = visitNodeInner(result.to, fn, ancestors) as Ast.For['to']; 36 | } 37 | if (result.times != null) { 38 | result.times = visitNodeInner(result.times, fn, ancestors) as Ast.For['times']; 39 | } 40 | result.for = visitNodeInner(result.for, fn, ancestors) as Ast.For['for']; 41 | break; 42 | } 43 | case 'loop': { 44 | for (let i = 0; i < result.statements.length; i++) { 45 | result.statements[i] = visitNodeInner(result.statements[i]!, fn, ancestors) as Ast.Loop['statements'][number]; 46 | } 47 | break; 48 | } 49 | case 'addAssign': 50 | case 'subAssign': 51 | case 'assign': { 52 | result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Assign['expr']; 53 | result.dest = visitNodeInner(result.dest, fn, ancestors) as Ast.Assign['dest']; 54 | break; 55 | } 56 | case 'plus': { 57 | result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Plus['expr']; 58 | break; 59 | } 60 | case 'minus': { 61 | result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Minus['expr']; 62 | break; 63 | } 64 | case 'not': { 65 | result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Not['expr']; 66 | break; 67 | } 68 | case 'if': { 69 | result.cond = visitNodeInner(result.cond, fn, ancestors) as Ast.If['cond']; 70 | result.then = visitNodeInner(result.then, fn, ancestors) as Ast.If['then']; 71 | for (const prop of result.elseif) { 72 | prop.cond = visitNodeInner(prop.cond, fn, ancestors) as Ast.If['elseif'][number]['cond']; 73 | prop.then = visitNodeInner(prop.then, fn, ancestors) as Ast.If['elseif'][number]['then']; 74 | } 75 | if (result.else != null) { 76 | result.else = visitNodeInner(result.else, fn, ancestors) as Ast.If['else']; 77 | } 78 | break; 79 | } 80 | case 'fn': { 81 | for (const param of result.params) { 82 | if (param.default) { 83 | param.default = visitNodeInner(param.default!, fn, ancestors) as Ast.Fn['params'][number]['default']; 84 | } 85 | if (param.argType != null) { 86 | param.argType = visitNodeInner(param.argType, fn, ancestors) as Ast.Fn['params'][number]['argType']; 87 | } 88 | } 89 | if (result.retType != null) { 90 | result.retType = visitNodeInner(result.retType, fn, ancestors) as Ast.Fn['retType']; 91 | } 92 | for (let i = 0; i < result.children.length; i++) { 93 | result.children[i] = visitNodeInner(result.children[i]!, fn, ancestors) as Ast.Fn['children'][number]; 94 | } 95 | break; 96 | } 97 | case 'match': { 98 | result.about = visitNodeInner(result.about, fn, ancestors) as Ast.Match['about']; 99 | for (const prop of result.qs) { 100 | prop.q = visitNodeInner(prop.q, fn, ancestors) as Ast.Match['qs'][number]['q']; 101 | prop.a = visitNodeInner(prop.a, fn, ancestors) as Ast.Match['qs'][number]['a']; 102 | } 103 | if (result.default != null) { 104 | result.default = visitNodeInner(result.default, fn, ancestors) as Ast.Match['default']; 105 | } 106 | break; 107 | } 108 | case 'block': { 109 | for (let i = 0; i < result.statements.length; i++) { 110 | result.statements[i] = visitNodeInner(result.statements[i]!, fn, ancestors) as Ast.Block['statements'][number]; 111 | } 112 | break; 113 | } 114 | case 'exists': { 115 | result.identifier = visitNodeInner(result.identifier, fn, ancestors) as Ast.Exists['identifier']; 116 | break; 117 | } 118 | case 'tmpl': { 119 | for (let i = 0; i < result.tmpl.length; i++) { 120 | const item = result.tmpl[i]!; 121 | if (typeof item !== 'string') { 122 | result.tmpl[i] = visitNodeInner(item, fn, ancestors) as Ast.Tmpl['tmpl'][number]; 123 | } 124 | } 125 | break; 126 | } 127 | case 'obj': { 128 | for (const item of result.value) { 129 | result.value.set(item[0], visitNodeInner(item[1], fn, ancestors) as Ast.Expression); 130 | } 131 | break; 132 | } 133 | case 'arr': { 134 | for (let i = 0; i < result.value.length; i++) { 135 | result.value[i] = visitNodeInner(result.value[i]!, fn, ancestors) as Ast.Arr['value'][number]; 136 | } 137 | break; 138 | } 139 | case 'call': { 140 | result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Call['target']; 141 | for (let i = 0; i < result.args.length; i++) { 142 | result.args[i] = visitNodeInner(result.args[i]!, fn, ancestors) as Ast.Call['args'][number]; 143 | } 144 | break; 145 | } 146 | case 'index': { 147 | result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Index['target']; 148 | result.index = visitNodeInner(result.index, fn, ancestors) as Ast.Index['index']; 149 | break; 150 | } 151 | case 'prop': { 152 | result.target = visitNodeInner(result.target, fn, ancestors) as Ast.Prop['target']; 153 | break; 154 | } 155 | case 'ns': { 156 | for (let i = 0; i < result.members.length; i++) { 157 | result.members[i] = visitNodeInner(result.members[i]!, fn, ancestors) as (typeof result.members)[number]; 158 | } 159 | break; 160 | } 161 | 162 | case 'pow': 163 | case 'mul': 164 | case 'div': 165 | case 'rem': 166 | case 'add': 167 | case 'sub': 168 | case 'lt': 169 | case 'lteq': 170 | case 'gt': 171 | case 'gteq': 172 | case 'eq': 173 | case 'neq': 174 | case 'and': 175 | case 'or': { 176 | result.left = visitNodeInner(result.left, fn, ancestors) as ( 177 | Ast.Pow | 178 | Ast.Mul | 179 | Ast.Div | 180 | Ast.Rem | 181 | Ast.Add | 182 | Ast.Sub | 183 | Ast.Lt | 184 | Ast.Lteq | 185 | Ast.Gt | 186 | Ast.Gteq | 187 | Ast.Eq | 188 | Ast.Neq | 189 | Ast.And | 190 | Ast.Or 191 | )['left']; 192 | result.right = visitNodeInner(result.right, fn, ancestors) as ( 193 | Ast.Pow | 194 | Ast.Mul | 195 | Ast.Div | 196 | Ast.Rem | 197 | Ast.Add | 198 | Ast.Sub | 199 | Ast.Lt | 200 | Ast.Lteq | 201 | Ast.Gt | 202 | Ast.Gteq | 203 | Ast.Eq | 204 | Ast.Neq | 205 | Ast.And | 206 | Ast.Or 207 | )['right']; 208 | break; 209 | } 210 | 211 | case 'fnTypeSource': { 212 | for (let i = 0; i < result.params.length; i++) { 213 | result.params[i] = visitNodeInner(result.params[i]!, fn, ancestors) as Ast.FnTypeSource['params'][number]; 214 | } 215 | break; 216 | } 217 | case 'unionTypeSource': { 218 | for (let i = 0; i < result.inners.length; i++) { 219 | result.inners[i] = visitNodeInner(result.inners[i]!, fn, ancestors) as Ast.UnionTypeSource['inners'][number]; 220 | } 221 | break; 222 | } 223 | } 224 | 225 | ancestors.pop(); 226 | return result; 227 | } 228 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import { AiScriptSyntaxError } from './error.js'; 2 | import type * as Ast from './node.js'; 3 | 4 | // Type (Semantic analyzed) 5 | 6 | export type TSimple = { 7 | type: 'simple'; 8 | name: N; 9 | } 10 | 11 | export function T_SIMPLE(name: T): TSimple { 12 | return { 13 | type: 'simple', 14 | name: name, 15 | }; 16 | } 17 | 18 | export function isAny(x: Type): x is TSimple<'any'> { 19 | return x.type === 'simple' && x.name === 'any'; 20 | } 21 | 22 | export type TGeneric = { 23 | type: 'generic'; 24 | name: N; 25 | inners: Type[]; 26 | } 27 | 28 | export function T_GENERIC(name: N, inners: Type[]): TGeneric { 29 | return { 30 | type: 'generic', 31 | name: name, 32 | inners: inners, 33 | }; 34 | } 35 | 36 | export type TFn = { 37 | type: 'fn'; 38 | params: Type[]; 39 | result: Type; 40 | }; 41 | 42 | export function T_FN(params: Type[], result: Type): TFn { 43 | return { 44 | type: 'fn', 45 | params, 46 | result, 47 | }; 48 | } 49 | 50 | export type TParam = { 51 | type: 'param'; 52 | name: string; 53 | } 54 | 55 | export function T_PARAM(name: string): TParam { 56 | return { 57 | type: 'param', 58 | name, 59 | }; 60 | } 61 | 62 | export type TUnion = { 63 | type: 'union'; 64 | inners: Type[]; 65 | } 66 | 67 | export function T_UNION(inners: Type[]): TUnion { 68 | return { 69 | type: 'union', 70 | inners, 71 | }; 72 | } 73 | 74 | export type Type = TSimple | TGeneric | TFn | TParam | TUnion; 75 | 76 | function assertTSimple(t: Type): asserts t is TSimple { if (t.type !== 'simple') { throw new TypeError('assertTSimple failed.'); } } 77 | function assertTGeneric(t: Type): asserts t is TGeneric { if (t.type !== 'generic') { throw new TypeError('assertTGeneric failed.'); } } 78 | function assertTFn(t: Type): asserts t is TFn { if (t.type !== 'fn') { throw new TypeError('assertTFn failed.'); } } 79 | 80 | // Utility 81 | 82 | export function isCompatibleType(a: Type, b: Type): boolean { 83 | if (isAny(a) || isAny(b)) return true; 84 | if (a.type !== b.type) return false; 85 | 86 | switch (a.type) { 87 | case 'simple': { 88 | assertTSimple(b); // NOTE: TypeGuardが効かない 89 | if (a.name !== b.name) return false; 90 | break; 91 | } 92 | case 'generic': { 93 | assertTGeneric(b); // NOTE: TypeGuardが効かない 94 | // name 95 | if (a.name !== b.name) return false; 96 | // inners 97 | if (a.inners.length !== b.inners.length) return false; 98 | for (let i = 0; i < a.inners.length; i++) { 99 | if (!isCompatibleType(a.inners[i]!, b.inners[i]!)) return false; 100 | } 101 | break; 102 | } 103 | case 'fn': { 104 | assertTFn(b); // NOTE: TypeGuardが効かない 105 | // fn result 106 | if (!isCompatibleType(a.result, b.result)) return false; 107 | // fn parameters 108 | if (a.params.length !== b.params.length) return false; 109 | for (let i = 0; i < a.params.length; i++) { 110 | if (!isCompatibleType(a.params[i]!, b.params[i]!)) return false; 111 | } 112 | break; 113 | } 114 | case 'param': { 115 | // TODO 116 | break; 117 | } 118 | case 'union': { 119 | // TODO 120 | break; 121 | } 122 | } 123 | 124 | return true; 125 | } 126 | 127 | export function getTypeName(type: Type): string { 128 | switch (type.type) { 129 | case 'simple': { 130 | return type.name; 131 | } 132 | case 'generic': { 133 | return `${type.name}<${type.inners.map(inner => getTypeName(inner)).join(', ')}>`; 134 | } 135 | case 'fn': { 136 | return `@(${type.params.map(param => getTypeName(param)).join(', ')}) { ${getTypeName(type.result)} }`; 137 | } 138 | case 'param': { 139 | return type.name; 140 | } 141 | case 'union': { 142 | return type.inners.join(' | '); 143 | } 144 | } 145 | } 146 | 147 | export function getTypeNameBySource(typeSource: Ast.TypeSource): string { 148 | switch (typeSource.type) { 149 | case 'namedTypeSource': { 150 | if (typeSource.inner) { 151 | const inner = getTypeNameBySource(typeSource.inner); 152 | return `${typeSource.name}<${inner}>`; 153 | } else { 154 | return typeSource.name; 155 | } 156 | } 157 | case 'fnTypeSource': { 158 | const params = typeSource.params.map(param => getTypeNameBySource(param)).join(', '); 159 | const result = getTypeNameBySource(typeSource.result); 160 | return `@(${params}) { ${result} }`; 161 | } 162 | case 'unionTypeSource': { 163 | return typeSource.inners.map(inner => getTypeBySource(inner)).join(' | '); 164 | } 165 | } 166 | } 167 | 168 | export function getTypeBySource(typeSource: Ast.TypeSource, typeParams?: readonly Ast.TypeParam[]): Type { 169 | if (typeSource.type === 'namedTypeSource') { 170 | const typeParam = typeParams?.find((param) => param.name === typeSource.name); 171 | if (typeParam != null) { 172 | return T_PARAM(typeParam.name); 173 | } 174 | 175 | switch (typeSource.name) { 176 | // simple types 177 | case 'null': 178 | case 'bool': 179 | case 'num': 180 | case 'str': 181 | case 'error': 182 | case 'never': 183 | case 'any': 184 | case 'void': { 185 | if (typeSource.inner == null) { 186 | return T_SIMPLE(typeSource.name); 187 | } 188 | break; 189 | } 190 | // alias for Generic types 191 | case 'arr': 192 | case 'obj': { 193 | let innerType: Type; 194 | if (typeSource.inner != null) { 195 | innerType = getTypeBySource(typeSource.inner, typeParams); 196 | } else { 197 | innerType = T_SIMPLE('any'); 198 | } 199 | return T_GENERIC(typeSource.name, [innerType]); 200 | } 201 | } 202 | throw new AiScriptSyntaxError(`Unknown type: '${getTypeNameBySource(typeSource)}'`, typeSource.loc.start); 203 | } else if (typeSource.type === 'fnTypeSource') { 204 | let fnTypeParams = typeSource.typeParams; 205 | if (typeParams != null) { 206 | fnTypeParams = fnTypeParams.concat(typeParams); 207 | } 208 | const paramTypes = typeSource.params.map(param => getTypeBySource(param, fnTypeParams)); 209 | return T_FN(paramTypes, getTypeBySource(typeSource.result, fnTypeParams)); 210 | } else { 211 | const innerTypes = typeSource.inners.map(inner => getTypeBySource(inner, typeParams)); 212 | return T_UNION(innerTypes); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/utils/mini-autobind.ts: -------------------------------------------------------------------------------- 1 | export function autobind unknown>(target: object, key: string | symbol, descriptor: TypedPropertyDescriptor): TypedPropertyDescriptor { 2 | let fn = descriptor.value!; 3 | 4 | return { 5 | configurable: true, 6 | get(): T { 7 | const bound = fn.bind(this); 8 | 9 | Object.defineProperty(this, key, { 10 | configurable: true, 11 | writable: true, 12 | value: bound, 13 | }); 14 | 15 | return bound; 16 | }, 17 | set(newFn: T): void { 18 | fn = newFn; 19 | }, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/random/CryptoGen.ts: -------------------------------------------------------------------------------- 1 | import { RandomBase, readBigUintLittleEndian } from './randomBase.js'; 2 | 3 | export class CryptoGen extends RandomBase { 4 | private static _instance: CryptoGen = new CryptoGen(); 5 | public static get instance() : CryptoGen { 6 | return CryptoGen._instance; 7 | } 8 | 9 | private constructor() { 10 | super(); 11 | } 12 | 13 | protected generateBigUintByBytes(bytes: number): bigint { 14 | let u8a = new Uint8Array(Math.ceil(bytes / 8) * 8); 15 | if (u8a.length < 1 || !Number.isSafeInteger(bytes)) return 0n; 16 | u8a = this.generateBytes(u8a.subarray(0, bytes)); 17 | return readBigUintLittleEndian(u8a.buffer) ?? 0n; 18 | } 19 | 20 | public generateBigUintByBits(bits: number): bigint { 21 | if (bits < 1 || !Number.isSafeInteger(bits)) return 0n; 22 | const bytes = Math.ceil(bits / 8); 23 | const wastedBits = BigInt(bytes * 8 - bits); 24 | return this.generateBigUintByBytes(bytes) >> wastedBits; 25 | } 26 | public generateBytes(array: Uint8Array): Uint8Array { 27 | return crypto.getRandomValues(array); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/random/chacha20.ts: -------------------------------------------------------------------------------- 1 | import { RandomBase, readBigUintLittleEndian } from './randomBase.js'; 2 | 3 | // translated from https://github.com/skeeto/chacha-js/blob/master/chacha.js 4 | const chacha20BlockSize = 64; 5 | const CHACHA_ROUNDS = 20; 6 | const CHACHA_KEYSIZE = 32; 7 | const CHACHA_IVSIZE = 8; 8 | function rotate(v: number, n: number): number { return (v << n) | (v >>> (32 - n)); } 9 | function quarterRound(x: Uint32Array, a: number, b: number, c: number, d: number): void { 10 | if (x.length < 16) return; 11 | let va = x[a]; 12 | let vb = x[b]; 13 | let vc = x[c]; 14 | let vd = x[d]; 15 | if (va === undefined || vb === undefined || vc === undefined || vd === undefined) return; 16 | va = (va + vb) | 0; 17 | vd = rotate(vd ^ va, 16); 18 | vc = (vc + vd) | 0; 19 | vb = rotate(vb ^ vc, 12); 20 | va = (va + vb) | 0; 21 | vd = rotate(vd ^ va, 8); 22 | vc = (vc + vd) | 0; 23 | vb = rotate(vb ^ vc, 7); 24 | x[a] = va; 25 | x[b] = vb; 26 | x[c] = vc; 27 | x[d] = vd; 28 | } 29 | function generateChaCha20(dst: Uint32Array, state: Uint32Array) : void { 30 | if (dst.length < 16 || state.length < 16) return; 31 | dst.set(state); 32 | for (let i = 0; i < CHACHA_ROUNDS; i += 2) { 33 | quarterRound(dst, 0, 4, 8, 12); 34 | quarterRound(dst, 1, 5, 9, 13); 35 | quarterRound(dst, 2, 6, 10, 14); 36 | quarterRound(dst, 3, 7, 11, 15); 37 | quarterRound(dst, 0, 5, 10, 15); 38 | quarterRound(dst, 1, 6, 11, 12); 39 | quarterRound(dst, 2, 7, 8, 13); 40 | quarterRound(dst, 3, 4, 9, 14); 41 | } 42 | for (let i = 0; i < 16; i++) { 43 | let d = dst[i]; 44 | const s = state[i]; 45 | if (d === undefined || s === undefined) throw new Error('generateChaCha20: Something went wrong!'); 46 | d = (d + s) | 0; 47 | dst[i] = d; 48 | } 49 | } 50 | export class ChaCha20 extends RandomBase { 51 | private keynonce: Uint32Array; 52 | private state: Uint32Array; 53 | private buffer: Uint8Array; 54 | private filledBuffer: Uint8Array; 55 | private counter: bigint; 56 | constructor(seed?: Uint8Array | undefined) { 57 | const keyNonceBytes = CHACHA_IVSIZE + CHACHA_KEYSIZE; 58 | super(); 59 | let keynonce: Uint8Array; 60 | if (typeof seed === 'undefined') { 61 | keynonce = crypto.getRandomValues(new Uint8Array(keyNonceBytes)); 62 | } else { 63 | keynonce = seed; 64 | if (keynonce.byteLength > keyNonceBytes) keynonce = seed.subarray(0, keyNonceBytes); 65 | if (keynonce.byteLength < keyNonceBytes) { 66 | const y = new Uint8Array(keyNonceBytes); 67 | y.set(keynonce); 68 | keynonce = y; 69 | } 70 | } 71 | const key = keynonce.subarray(0, CHACHA_KEYSIZE); 72 | const nonce = keynonce.subarray(CHACHA_KEYSIZE, CHACHA_KEYSIZE + CHACHA_IVSIZE); 73 | const kn = new Uint8Array(16 * 4); 74 | kn.set([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]); 75 | kn.set(key, 4 * 4); 76 | kn.set(nonce, 14 * 4); 77 | this.keynonce = new Uint32Array(kn.buffer); 78 | this.state = new Uint32Array(16); 79 | this.buffer = new Uint8Array(chacha20BlockSize); 80 | this.counter = 0n; 81 | this.filledBuffer = new Uint8Array(0); 82 | } 83 | private fillBuffer(): void { 84 | this.buffer.fill(0); 85 | this.buffer = this.fillBufferDirect(this.buffer); 86 | this.filledBuffer = this.buffer; 87 | } 88 | private fillBufferDirect(buffer: Uint8Array): Uint8Array { 89 | if ((buffer.length % chacha20BlockSize) !== 0) throw new Error('ChaCha20.fillBufferDirect should always be called with the buffer with the length a multiple-of-64!'); 90 | buffer.fill(0); 91 | let counter = this.counter; 92 | const state = this.state; 93 | const counterState = new BigUint64Array(state.buffer); 94 | let dst = buffer; 95 | while (dst.length > 0) { 96 | const dbuf = dst.subarray(0, state.byteLength); 97 | const dst32 = new Uint32Array(dbuf.buffer); 98 | state.set(this.keynonce); 99 | counterState[6] = BigInt.asUintN(64, counter); 100 | generateChaCha20(dst32, state); 101 | dst = dst.subarray(dbuf.length); 102 | counter = BigInt.asUintN(64, counter + 1n); 103 | } 104 | this.counter = counter; 105 | return buffer; 106 | } 107 | 108 | protected generateBigUintByBytes(bytes: number): bigint { 109 | let u8a = new Uint8Array(Math.ceil(bytes / 8) * 8); 110 | if (u8a.length < 1 || !Number.isSafeInteger(bytes)) return 0n; 111 | u8a = this.generateBytes(u8a.subarray(0, bytes)); 112 | return readBigUintLittleEndian(u8a.buffer) ?? 0n; 113 | } 114 | 115 | public generateBigUintByBits(bits: number): bigint { 116 | if (bits < 1 || !Number.isSafeInteger(bits)) return 0n; 117 | const bytes = Math.ceil(bits / 8); 118 | const wastedBits = BigInt(bytes * 8 - bits); 119 | return this.generateBigUintByBytes(bytes) >> wastedBits; 120 | } 121 | 122 | public generateBytes(array: Uint8Array): Uint8Array { 123 | if (array.length < 1) return array; 124 | array.fill(0); 125 | let dst = array; 126 | if (dst.length <= this.filledBuffer.length) { 127 | dst.set(this.filledBuffer.subarray(0, dst.length)); 128 | this.filledBuffer = this.filledBuffer.subarray(dst.length); 129 | return array; 130 | } else { 131 | while (dst.length > 0) { 132 | if (this.filledBuffer.length === 0) { 133 | if (dst.length >= chacha20BlockSize) { 134 | const df64 = dst.subarray(0, dst.length - (dst.length % chacha20BlockSize)); 135 | this.fillBufferDirect(df64); 136 | dst = dst.subarray(df64.length); 137 | continue; 138 | } 139 | this.fillBuffer(); 140 | } 141 | if (dst.length <= this.filledBuffer.length) { 142 | dst.set(this.filledBuffer.subarray(0, dst.length)); 143 | this.filledBuffer = this.filledBuffer.subarray(dst.length); 144 | return array; 145 | } 146 | dst.set(this.filledBuffer); 147 | dst = dst.subarray(this.filledBuffer.length); 148 | this.fillBuffer(); 149 | } 150 | return array; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/utils/random/genrng.ts: -------------------------------------------------------------------------------- 1 | import seedrandom from 'seedrandom'; 2 | import { FN_NATIVE, NULL, NUM } from '../../interpreter/value.js'; 3 | import { textEncoder } from '../../const.js'; 4 | import { SeedRandomWrapper } from './seedrandom.js'; 5 | import { ChaCha20 } from './chacha20.js'; 6 | import type { VNativeFn, VNull, Value } from '../../interpreter/value.js'; 7 | 8 | export function GenerateLegacyRandom(seed: Value | undefined) : VNativeFn | VNull { 9 | if (!seed || seed.type !== 'num' && seed.type !== 'str') return NULL; 10 | const rng = seedrandom(seed.value.toString()); 11 | return FN_NATIVE(([min, max]) => { 12 | if (min && min.type === 'num' && max && max.type === 'num') { 13 | return NUM(Math.floor(rng() * (Math.floor(max.value) - Math.ceil(min.value) + 1) + Math.ceil(min.value))); 14 | } 15 | return NUM(rng()); 16 | }); 17 | } 18 | 19 | export function GenerateRC4Random(seed: Value | undefined) : VNativeFn | VNull { 20 | if (!seed || seed.type !== 'num' && seed.type !== 'str') return NULL; 21 | const rng = new SeedRandomWrapper(seed.value); 22 | return FN_NATIVE(([min, max]) => { 23 | if (min && min.type === 'num' && max && max.type === 'num') { 24 | const result = rng.generateRandomIntegerInRange(min.value, max.value); 25 | return typeof result === 'number' ? NUM(result) : NULL; 26 | } 27 | return NUM(rng.generateNumber0To1()); 28 | }); 29 | } 30 | 31 | export async function GenerateChaCha20Random(seed: Value | undefined) : Promise { 32 | if (!seed || seed.type !== 'num' && seed.type !== 'str' && seed.type !== 'null') return NULL; 33 | let actualSeed : Uint8Array | undefined = undefined; 34 | if (seed.type === 'num') 35 | { 36 | actualSeed = new Uint8Array(await crypto.subtle.digest('SHA-384', new Uint8Array(new Float64Array([seed.value])))); 37 | } else if (seed.type === 'str') { 38 | actualSeed = new Uint8Array(await crypto.subtle.digest('SHA-384', new Uint8Array(textEncoder.encode(seed.value)))); 39 | } 40 | const rng = new ChaCha20(actualSeed); 41 | return FN_NATIVE(([min, max]) => { 42 | if (min && min.type === 'num' && max && max.type === 'num') { 43 | const result = rng.generateRandomIntegerInRange(min.value, max.value); 44 | return typeof result === 'number' ? NUM(result) : NULL; 45 | } 46 | return NUM(rng.generateNumber0To1()); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/random/randomBase.ts: -------------------------------------------------------------------------------- 1 | export const safeIntegerBits = Math.ceil(Math.log2(Number.MAX_SAFE_INTEGER)); 2 | export const bigSafeIntegerBits = BigInt(safeIntegerBits); 3 | export const bigMaxSafeIntegerExclusive = 1n << bigSafeIntegerBits; 4 | export const fractionBits = safeIntegerBits - 1; 5 | export const bigFractionBits = BigInt(fractionBits); 6 | 7 | export abstract class RandomBase { 8 | protected abstract generateBigUintByBytes(bytes: number): bigint; 9 | public abstract generateBigUintByBits(bits: number): bigint; 10 | public abstract generateBytes(array: Uint8Array): Uint8Array; 11 | 12 | public generateNumber0To1(): number { 13 | let res = this.generateBigUintByBits(safeIntegerBits); 14 | let exponent = 1022; 15 | let remainingFractionBits = safeIntegerBits - bitsToRepresent(res); 16 | while (remainingFractionBits > 0 && exponent >= safeIntegerBits) { 17 | exponent -= remainingFractionBits; 18 | res <<= BigInt(remainingFractionBits); 19 | res |= this.generateBigUintByBits(remainingFractionBits); 20 | remainingFractionBits = safeIntegerBits - bitsToRepresent(res); 21 | } 22 | if (remainingFractionBits > 0) { 23 | const shift = Math.min(exponent - 1, remainingFractionBits); 24 | res <<= BigInt(shift); 25 | res |= this.generateBigUintByBits(shift); 26 | exponent = Math.max(exponent - shift, 0); 27 | } 28 | return (Number(res) * 0.5 ** safeIntegerBits) * (0.5 ** (1022 - exponent)); 29 | } 30 | 31 | public generateUniform(maxInclusive: bigint): bigint { 32 | if (maxInclusive < 1) return 0n; 33 | const log2 = maxInclusive.toString(2).length; 34 | const bytes = Math.ceil(log2 / 8); 35 | const wastedBits = BigInt(bytes * 8 - log2); 36 | let result: bigint; 37 | do { 38 | result = this.generateBigUintByBytes(bytes) >> wastedBits; 39 | } while (result > maxInclusive); 40 | return result; 41 | } 42 | 43 | public generateRandomIntegerInRange(min: number, max: number): number | null { 44 | const ceilMin = Math.ceil(min); 45 | const floorMax = Math.floor(max); 46 | const signedScale = floorMax - ceilMin; 47 | if (signedScale === 0) return ceilMin; 48 | const scale = Math.abs(signedScale); 49 | const scaleSign = Math.sign(signedScale); 50 | if (!Number.isSafeInteger(scale) || !Number.isSafeInteger(ceilMin) || !Number.isSafeInteger(floorMax)) { 51 | return null; 52 | } 53 | const bigScale = BigInt(scale); 54 | return Number(this.generateUniform(bigScale)) * scaleSign + ceilMin; 55 | } 56 | } 57 | 58 | export function bitsToRepresent(num: bigint): number { 59 | if (num === 0n) return 0; 60 | return num.toString(2).length; 61 | } 62 | 63 | function readSmallBigUintLittleEndian(buffer: ArrayBufferLike): bigint | null { 64 | if (buffer.byteLength === 0) return null; 65 | if (buffer.byteLength < 8) { 66 | const array = new Uint8Array(8); 67 | array.set(new Uint8Array(buffer)); 68 | return new DataView(array.buffer).getBigUint64(0, true); 69 | } 70 | return new DataView(buffer).getBigUint64(0, true); 71 | } 72 | 73 | export function readBigUintLittleEndian(buffer: ArrayBufferLike): bigint | null { 74 | if (buffer.byteLength === 0) return null; 75 | if (buffer.byteLength <= 8) { 76 | return readSmallBigUintLittleEndian(buffer); 77 | } 78 | const dataView = new DataView(buffer); 79 | let pos = 0n; 80 | let res = 0n; 81 | let index = 0; 82 | for (; index < dataView.byteLength - 7; index += 8, pos += 64n) { 83 | const element = dataView.getBigUint64(index, true); 84 | res |= element << pos; 85 | } 86 | if (index < dataView.byteLength) { 87 | const array = new Uint8Array(8); 88 | array.set(new Uint8Array(buffer, index)); 89 | res |= new DataView(array.buffer).getBigUint64(0, true) << pos; 90 | } 91 | return res; 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/random/seedrandom.ts: -------------------------------------------------------------------------------- 1 | import seedrandom from 'seedrandom'; 2 | import { RandomBase, readBigUintLittleEndian } from './randomBase.js'; 3 | 4 | const seedRandomBlockSize = Int32Array.BYTES_PER_ELEMENT; 5 | 6 | export class SeedRandomWrapper extends RandomBase { 7 | private rng: seedrandom.PRNG; 8 | private buffer: Uint8Array; 9 | private filledBuffer: Uint8Array; 10 | constructor(seed: string | number) { 11 | super(); 12 | this.rng = seedrandom(seed.toString()); 13 | this.buffer = new Uint8Array(seedRandomBlockSize); 14 | this.filledBuffer = new Uint8Array(0); 15 | } 16 | private fillBuffer(): void { 17 | this.buffer.fill(0); 18 | this.buffer = this.fillBufferDirect(this.buffer); 19 | this.filledBuffer = this.buffer; 20 | } 21 | private fillBufferDirect(buffer: Uint8Array): Uint8Array { 22 | if ((buffer.length % seedRandomBlockSize) !== 0) throw new Error(`SeedRandomWrapper.fillBufferDirect should always be called with the buffer with the length a multiple-of-${seedRandomBlockSize}!`); 23 | const length = buffer.length / seedRandomBlockSize; 24 | const dataView = new DataView(buffer.buffer); 25 | let byteOffset = 0; 26 | for (let index = 0; index < length; index++, byteOffset += seedRandomBlockSize) { 27 | dataView.setInt32(byteOffset, this.rng.int32(), false); 28 | } 29 | return buffer; 30 | } 31 | protected generateBigUintByBytes(bytes: number): bigint { 32 | let u8a = new Uint8Array(Math.ceil(bytes / 8) * 8); 33 | if (u8a.length < 1 || !Number.isSafeInteger(bytes)) return 0n; 34 | u8a = this.generateBytes(u8a.subarray(0, bytes)); 35 | return readBigUintLittleEndian(u8a.buffer) ?? 0n; 36 | } 37 | 38 | public generateBigUintByBits(bits: number): bigint { 39 | if (bits < 1 || !Number.isSafeInteger(bits)) return 0n; 40 | const bytes = Math.ceil(bits / 8); 41 | const wastedBits = BigInt(bytes * 8 - bits); 42 | return this.generateBigUintByBytes(bytes) >> wastedBits; 43 | } 44 | 45 | public generateBytes(array: Uint8Array): Uint8Array { 46 | if (array.length < 1) return array; 47 | array.fill(0); 48 | let dst = array; 49 | if (dst.length <= this.filledBuffer.length) { 50 | dst.set(this.filledBuffer.subarray(0, dst.length)); 51 | this.filledBuffer = this.filledBuffer.subarray(dst.length); 52 | return array; 53 | } else { 54 | while (dst.length > 0) { 55 | if (this.filledBuffer.length === 0) { 56 | if (dst.length >= seedRandomBlockSize) { 57 | const df64 = dst.subarray(0, dst.length - (dst.length % seedRandomBlockSize)); 58 | this.fillBufferDirect(df64); 59 | dst = dst.subarray(df64.length); 60 | continue; 61 | } 62 | this.fillBuffer(); 63 | } 64 | if (dst.length <= this.filledBuffer.length) { 65 | dst.set(this.filledBuffer.subarray(0, dst.length)); 66 | this.filledBuffer = this.filledBuffer.subarray(dst.length); 67 | return array; 68 | } 69 | dst.set(this.filledBuffer); 70 | dst = dst.subarray(this.filledBuffer.length); 71 | this.fillBuffer(); 72 | } 73 | return array; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/interpreter.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest'; 3 | import { Parser, Interpreter, values, errors, utils, Ast } from '../src'; 4 | 5 | let { FN_NATIVE } = values; 6 | let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError, AiScriptHostsideError } = errors; 7 | 8 | describe('Scope', () => { 9 | test.concurrent('getAll', async () => { 10 | const aiscript = new Interpreter({}); 11 | await aiscript.exec(Parser.parse(` 12 | let a = 1 13 | @b() { 14 | let x = a + 1 15 | x 16 | } 17 | if true { 18 | var y = 2 19 | } 20 | var c = true 21 | `)); 22 | const vars = aiscript.scope.getAll(); 23 | assert.ok(vars.get('a') != null); 24 | assert.ok(vars.get('b') != null); 25 | assert.ok(vars.get('c') != null); 26 | assert.ok(vars.get('x') == null); 27 | assert.ok(vars.get('y') == null); 28 | }); 29 | }); 30 | 31 | describe('error handler', () => { 32 | test.concurrent('error from outside caller', async () => { 33 | let outsideCaller: () => Promise = async () => {}; 34 | let errCount: number = 0; 35 | const aiscript = new Interpreter({ 36 | emitError: FN_NATIVE((_args, _opts) => { 37 | throw Error('emitError'); 38 | }), 39 | genOutsideCaller: FN_NATIVE(([fn], opts) => { 40 | utils.assertFunction(fn); 41 | outsideCaller = async () => { 42 | opts.topCall(fn, []); 43 | }; 44 | }), 45 | }, { 46 | err(e) { /*console.log(e.toString());*/ errCount++ }, 47 | }); 48 | await aiscript.exec(Parser.parse(` 49 | genOutsideCaller(emitError) 50 | `)); 51 | assert.strictEqual(errCount, 0); 52 | await outsideCaller(); 53 | assert.strictEqual(errCount, 1); 54 | }); 55 | 56 | test.concurrent('array.map calls the handler just once', async () => { 57 | let errCount: number = 0; 58 | const aiscript = new Interpreter({}, { 59 | err(e) { errCount++ }, 60 | }); 61 | await aiscript.exec(Parser.parse(` 62 | Core:range(1,5).map(@(){ hoge }) 63 | `)); 64 | assert.strictEqual(errCount, 1); 65 | }); 66 | }); 67 | 68 | describe('error location', () => { 69 | const exeAndGetErrPos = (src: string): Promise => new Promise((ok, ng) => { 70 | const aiscript = new Interpreter({ 71 | emitError: FN_NATIVE((_args, _opts) => { 72 | throw Error('emitError'); 73 | }), 74 | }, { 75 | err(e) { ok(e.pos) }, 76 | }); 77 | aiscript.exec(Parser.parse(src)).then(() => ng('error has not occured.')); 78 | }); 79 | 80 | test.concurrent('Non-aiscript Error', async () => { 81 | return expect(exeAndGetErrPos(`/* (の位置 82 | */ 83 | emitError() 84 | `)).resolves.toEqual({ line: 3, column: 13}); 85 | }); 86 | 87 | test.concurrent('No "var" in namespace declaration', async () => { 88 | return expect(exeAndGetErrPos(`// vの位置 89 | :: Ai { 90 | let chan = 'kawaii' 91 | var kun = '!?' 92 | } 93 | `)).resolves.toEqual({ line: 4, column: 5}); 94 | }); 95 | 96 | test.concurrent('Index out of range', async () => { 97 | return expect(exeAndGetErrPos(`// [の位置 98 | let arr = [] 99 | arr[0] 100 | `)).resolves.toEqual({ line: 3, column: 7}); 101 | }); 102 | 103 | test.concurrent('Error in passed function', async () => { 104 | return expect(exeAndGetErrPos(`// (の位置 105 | [1, 2, 3].map(@(v){ 106 | if v==1 Core:abort("error") 107 | }) 108 | `)).resolves.toEqual({ line: 3, column: 23}); 109 | }); 110 | 111 | test.concurrent('No such prop', async () => { 112 | return expect(exeAndGetErrPos(`// .の位置 113 | [].ai 114 | `)).resolves.toEqual({ line: 2, column: 6}); 115 | }); 116 | }); 117 | 118 | describe('callstack', () => { 119 | const exeAndGetErrMessage = (src: string): Promise => new Promise((ok, ng) => { 120 | const aiscript = new Interpreter({ 121 | emitError: FN_NATIVE((_args, _opts) => { 122 | throw Error('emitError'); 123 | }), 124 | }, { 125 | err(e) { ok(e.message) }, 126 | }); 127 | aiscript.exec(Parser.parse(src)).then(() => ng('error has not occurred.')); 128 | }); 129 | 130 | test('error in function', async () => { 131 | const result = await exeAndGetErrMessage(` 132 | @function1() { emitError() } 133 | @function2() { function1() } 134 | function2() 135 | `); 136 | expect(result).toMatchInlineSnapshot(` 137 | "emitError 138 | at function1 (Line 2, Column 28) 139 | at function2 (Line 3, Column 28) 140 | at (Line 4, Column 13)" 141 | `); 142 | }); 143 | test('error in function in namespace', async () => { 144 | const result = await exeAndGetErrMessage(` 145 | :: Ai { 146 | @function() { emitError() } 147 | } 148 | Ai:function() 149 | `); 150 | expect(result).toMatchInlineSnapshot(` 151 | "emitError 152 | at Ai:function (Line 3, Column 28) 153 | at (Line 5, Column 15)" 154 | `); 155 | }); 156 | test('error in anonymous function', async () => { 157 | const result = await exeAndGetErrMessage(` 158 | (@() { emitError() })() 159 | `); 160 | expect(result).toMatchInlineSnapshot(` 161 | "emitError 162 | at (Line 2, Column 20) 163 | at (Line 2, Column 25)" 164 | `); 165 | }); 166 | }); 167 | 168 | describe('IRQ', () => { 169 | describe('irqSleep is function', () => { 170 | async function countSleeps(irqRate: number): Promise { 171 | let count = 0; 172 | const interpreter = new Interpreter({}, { 173 | irqRate, 174 | // It's safe only when no massive loop occurs 175 | irqSleep: async () => count++, 176 | }); 177 | await interpreter.exec(Parser.parse(` 178 | 'Ai-chan kawaii' 179 | 'Ai-chan kawaii' 180 | 'Ai-chan kawaii' 181 | 'Ai-chan kawaii' 182 | 'Ai-chan kawaii' 183 | 'Ai-chan kawaii' 184 | 'Ai-chan kawaii' 185 | 'Ai-chan kawaii' 186 | 'Ai-chan kawaii' 187 | 'Ai-chan kawaii'`)); 188 | return count; 189 | } 190 | 191 | test.concurrent.each([ 192 | [0, 0], 193 | [1, 10], 194 | [2, 5], 195 | [10, 1], 196 | [Infinity, 0], 197 | ])('rate = %d', async (rate, count) => { 198 | return expect(countSleeps(rate)).resolves.toEqual(count); 199 | }); 200 | 201 | test.concurrent.each( 202 | [-1, NaN], 203 | )('rate = %d', async (rate, count) => { 204 | return expect(countSleeps(rate)).rejects.toThrow(AiScriptHostsideError); 205 | }); 206 | }); 207 | 208 | describe('irqSleep is number', () => { 209 | // This function does IRQ 10 times so takes 10 * irqSleep milliseconds in sum when executed. 210 | async function countSleeps(irqSleep: number): Promise { 211 | const interpreter = new Interpreter({}, { 212 | irqRate: 1, 213 | irqSleep, 214 | }); 215 | await interpreter.exec(Parser.parse(` 216 | 'Ai-chan kawaii' 217 | 'Ai-chan kawaii' 218 | 'Ai-chan kawaii' 219 | 'Ai-chan kawaii' 220 | 'Ai-chan kawaii' 221 | 'Ai-chan kawaii' 222 | 'Ai-chan kawaii' 223 | 'Ai-chan kawaii' 224 | 'Ai-chan kawaii' 225 | 'Ai-chan kawaii'`)); 226 | } 227 | 228 | beforeEach(() => { 229 | vi.useFakeTimers(); 230 | }) 231 | 232 | afterEach(() => { 233 | vi.restoreAllMocks(); 234 | }) 235 | 236 | test('It ends', async () => { 237 | const countSleepsSpy = vi.fn(countSleeps); 238 | countSleepsSpy(100); 239 | await vi.advanceTimersByTimeAsync(1000); 240 | return expect(countSleepsSpy).toHaveResolved(); 241 | }); 242 | 243 | test('It takes time', async () => { 244 | const countSleepsSpy = vi.fn(countSleeps); 245 | countSleepsSpy(100); 246 | await vi.advanceTimersByTimeAsync(999); 247 | return expect(countSleepsSpy).not.toHaveResolved(); 248 | }); 249 | 250 | test.each( 251 | [-1, NaN] 252 | )('Invalid number: %d', (time) => { 253 | return expect(countSleeps(time)).rejects.toThrow(AiScriptHostsideError); 254 | }); 255 | }); 256 | }); 257 | 258 | describe('pause', () => { 259 | async function exePausable() { 260 | let count = 0; 261 | 262 | const interpreter = new Interpreter({ 263 | count: values.FN_NATIVE(() => { count++; }), 264 | }, {}); 265 | 266 | // await to catch errors 267 | await interpreter.exec(Parser.parse( 268 | `Async:interval(100, @() { count() })` 269 | )); 270 | 271 | return { 272 | pause: interpreter.pause, 273 | unpause: interpreter.unpause, 274 | getCount: () => count, 275 | resetCount: () => count = 0, 276 | }; 277 | } 278 | 279 | beforeEach(() => { 280 | vi.useFakeTimers(); 281 | }) 282 | 283 | afterEach(() => { 284 | vi.restoreAllMocks(); 285 | }) 286 | 287 | test('basic', async () => { 288 | const p = await exePausable(); 289 | await vi.advanceTimersByTimeAsync(500); 290 | p.pause(); 291 | await vi.advanceTimersByTimeAsync(400); 292 | return expect(p.getCount()).toEqual(5); 293 | }); 294 | 295 | test('unpause', async () => { 296 | const p = await exePausable(); 297 | await vi.advanceTimersByTimeAsync(500); 298 | p.pause(); 299 | await vi.advanceTimersByTimeAsync(400); 300 | p.unpause(); 301 | await vi.advanceTimersByTimeAsync(300); 302 | return expect(p.getCount()).toEqual(8); 303 | }); 304 | 305 | describe('randomly scheduled pausing', () => { 306 | function rnd(min: number, max: number): number { 307 | return Math.floor(min + (Math.random() * (max - min + 1))); 308 | } 309 | const schedule = Array(rnd(2, 10)).fill(0).map(() => rnd(1, 10) * 100); 310 | const title = schedule.map((v, i) => `${i % 2 ? 'un' : ''}pause ${v}`).join(', '); 311 | 312 | test(title, async () => { 313 | const p = await exePausable(); 314 | let answer = 0; 315 | for (const [i, v] of schedule.entries()) { 316 | if (i % 2) { 317 | p.unpause(); 318 | answer += v / 100; 319 | } 320 | else p.pause(); 321 | await vi.advanceTimersByTimeAsync(v); 322 | } 323 | return expect(p.getCount()).toEqual(answer); 324 | }); 325 | }); 326 | }); 327 | -------------------------------------------------------------------------------- /test/keywords.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { Parser } from '../src'; 3 | import { AiScriptSyntaxError } from '../src/error'; 4 | 5 | const reservedWords = [ 6 | // 使用中の語 7 | 'null', 8 | 'true', 9 | 'false', 10 | 'each', 11 | 'for', 12 | 'do', 13 | 'while', 14 | 'loop', 15 | 'break', 16 | 'continue', 17 | 'match', 18 | 'case', 19 | 'default', 20 | 'if', 21 | 'elif', 22 | 'else', 23 | 'return', 24 | 'eval', 25 | 'var', 26 | 'let', 27 | 'exists', 28 | 29 | // 使用予定の語 30 | // 文脈キーワードは識別子に利用できるため除外 31 | 'as', 32 | 'async', 33 | 'attr', 34 | 'attribute', 35 | 'await', 36 | 'catch', 37 | 'class', 38 | // 'const', 39 | 'component', 40 | 'constructor', 41 | // 'def', 42 | 'dictionary', 43 | 'enum', 44 | 'export', 45 | 'finally', 46 | 'fn', 47 | // 'func', 48 | // 'function', 49 | 'hash', 50 | 'in', 51 | 'interface', 52 | 'out', 53 | 'private', 54 | 'public', 55 | 'ref', 56 | 'static', 57 | 'struct', 58 | 'table', 59 | 'this', 60 | 'throw', 61 | 'trait', 62 | 'try', 63 | 'undefined', 64 | 'use', 65 | 'using', 66 | 'when', 67 | 'yield', 68 | 'import', 69 | 'is', 70 | 'meta', 71 | 'module', 72 | 'namespace', 73 | 'new', 74 | ] as const; 75 | 76 | const sampleCodes = Object.entries<(word: string) => string>({ 77 | variable: word => 78 | ` 79 | let ${word} = "ai" 80 | ${word} 81 | `, 82 | 83 | function: word => 84 | ` 85 | @${word}() { 'ai' } 86 | ${word}() 87 | `, 88 | 89 | attribute: word => 90 | ` 91 | #[${word} 1] 92 | @f() { 1 } 93 | `, 94 | 95 | namespace: word => 96 | ` 97 | :: ${word} { 98 | @f() { 1 } 99 | } 100 | ${word}:f() 101 | `, 102 | 103 | prop: word => 104 | ` 105 | let x = { ${word}: 1 } 106 | x.${word} 107 | `, 108 | 109 | meta: word => 110 | ` 111 | ### ${word} 1 112 | `, 113 | 114 | for: word => 115 | ` 116 | #${word}: for 1 {} 117 | `, 118 | 119 | each: word => 120 | ` 121 | #${word}: each let v, [0] {} 122 | `, 123 | 124 | while: word => 125 | ` 126 | #${word}: while false {} 127 | `, 128 | 129 | break: word => 130 | ` 131 | #${word}: for 1 { 132 | break #${word} 133 | } 134 | `, 135 | 136 | continue: word => 137 | ` 138 | #${word}: for 1 { 139 | continue #${word} 140 | } 141 | `, 142 | 143 | typeParam: word => 144 | ` 145 | @f<${word}>(x): ${word} { x } 146 | `, 147 | }); 148 | 149 | const parser = new Parser(); 150 | 151 | describe.each( 152 | sampleCodes 153 | )('reserved word validation on %s', (_, sampleCode) => { 154 | 155 | test.concurrent.each( 156 | reservedWords 157 | )('%s must be rejected', (word) => { 158 | expect(() => parser.parse(sampleCode(word))).toThrow(AiScriptSyntaxError); 159 | }); 160 | 161 | test.concurrent.each( 162 | reservedWords 163 | )('%scat must be allowed', (word) => { 164 | parser.parse(sampleCode(word+'cat')); 165 | }); 166 | 167 | }); 168 | -------------------------------------------------------------------------------- /test/literals.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { describe, test } from 'vitest'; 3 | import { } from '../src'; 4 | import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; 5 | import { } from '../src/error'; 6 | import { exe, eq } from './testutils'; 7 | 8 | describe('literal', () => { 9 | test.concurrent('string (single quote)', async () => { 10 | const res = await exe(` 11 | <: 'foo' 12 | `); 13 | eq(res, STR('foo')); 14 | }); 15 | 16 | test.concurrent('string (double quote)', async () => { 17 | const res = await exe(` 18 | <: "foo" 19 | `); 20 | eq(res, STR('foo')); 21 | }); 22 | 23 | test.concurrent('Escaped double quote', async () => { 24 | const res = await exe('<: "ai saw a note \\"bebeyo\\"."'); 25 | eq(res, STR('ai saw a note "bebeyo".')); 26 | }); 27 | 28 | test.concurrent('Escaped single quote', async () => { 29 | const res = await exe('<: \'ai saw a note \\\'bebeyo\\\'.\''); 30 | eq(res, STR('ai saw a note \'bebeyo\'.')); 31 | }); 32 | 33 | test.concurrent('bool (true)', async () => { 34 | const res = await exe(` 35 | <: true 36 | `); 37 | eq(res, BOOL(true)); 38 | }); 39 | 40 | test.concurrent('bool (false)', async () => { 41 | const res = await exe(` 42 | <: false 43 | `); 44 | eq(res, BOOL(false)); 45 | }); 46 | 47 | test.concurrent('number (Int)', async () => { 48 | const res = await exe(` 49 | <: 10 50 | `); 51 | eq(res, NUM(10)); 52 | }); 53 | 54 | test.concurrent('number (Float)', async () => { 55 | const res = await exe(` 56 | <: 0.5 57 | `); 58 | eq(res, NUM(0.5)); 59 | }); 60 | 61 | test.concurrent('arr (separated by comma)', async () => { 62 | const res = await exe(` 63 | <: [1, 2, 3] 64 | `); 65 | eq(res, ARR([NUM(1), NUM(2), NUM(3)])); 66 | }); 67 | 68 | test.concurrent('arr (separated by comma) (with trailing comma)', async () => { 69 | const res = await exe(` 70 | <: [1, 2, 3,] 71 | `); 72 | eq(res, ARR([NUM(1), NUM(2), NUM(3)])); 73 | }); 74 | 75 | test.concurrent('arr (separated by line break)', async () => { 76 | const res = await exe(` 77 | <: [ 78 | 1 79 | 2 80 | 3 81 | ] 82 | `); 83 | eq(res, ARR([NUM(1), NUM(2), NUM(3)])); 84 | }); 85 | 86 | test.concurrent('arr (separated by line break and comma)', async () => { 87 | const res = await exe(` 88 | <: [ 89 | 1, 90 | 2, 91 | 3 92 | ] 93 | `); 94 | eq(res, ARR([NUM(1), NUM(2), NUM(3)])); 95 | }); 96 | 97 | test.concurrent('arr (separated by line break and comma) (with trailing comma)', async () => { 98 | const res = await exe(` 99 | <: [ 100 | 1, 101 | 2, 102 | 3, 103 | ] 104 | `); 105 | eq(res, ARR([NUM(1), NUM(2), NUM(3)])); 106 | }); 107 | 108 | test.concurrent('obj (separated by comma)', async () => { 109 | const res = await exe(` 110 | <: { a: 1, b: 2, c: 3 } 111 | `); 112 | eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); 113 | }); 114 | 115 | test.concurrent('obj (separated by comma) (with trailing comma)', async () => { 116 | const res = await exe(` 117 | <: { a: 1, b: 2, c: 3, } 118 | `); 119 | eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); 120 | }); 121 | 122 | test.concurrent('obj (separated by line break)', async () => { 123 | const res = await exe(` 124 | <: { 125 | a: 1 126 | b: 2 127 | c: 3 128 | } 129 | `); 130 | eq(res, OBJ(new Map([['a', NUM(1)], ['b', NUM(2)], ['c', NUM(3)]]))); 131 | }); 132 | 133 | test.concurrent('obj (string key)', async () => { 134 | const res = await exe(` 135 | <: { 136 | "藍": 42, 137 | } 138 | `); 139 | eq(res, OBJ(new Map([['藍', NUM(42)]]))); 140 | }); 141 | 142 | test.concurrent('obj and arr (separated by line break)', async () => { 143 | const res = await exe(` 144 | <: { 145 | a: 1 146 | b: [ 147 | 1 148 | 2 149 | 3 150 | ] 151 | c: 3 152 | } 153 | `); 154 | eq(res, OBJ(new Map([ 155 | ['a', NUM(1)], 156 | ['b', ARR([NUM(1), NUM(2), NUM(3)])], 157 | ['c', NUM(3)] 158 | ]))); 159 | }); 160 | }); 161 | 162 | describe('Template syntax', () => { 163 | test.concurrent('Basic', async () => { 164 | const res = await exe(` 165 | let str = "kawaii" 166 | <: \`Ai is {str}!\` 167 | `); 168 | eq(res, STR('Ai is kawaii!')); 169 | }); 170 | 171 | test.concurrent('convert to str', async () => { 172 | const res = await exe(` 173 | <: \`1 + 1 = {(1 + 1)}\` 174 | `); 175 | eq(res, STR('1 + 1 = 2')); 176 | }); 177 | 178 | test.concurrent('invalid', async () => { 179 | try { 180 | await exe(` 181 | <: \`{hoge}\` 182 | `); 183 | } catch (e) { 184 | assert.ok(true); 185 | return; 186 | } 187 | assert.fail(); 188 | }); 189 | 190 | test.concurrent('Escape', async () => { 191 | const res = await exe(` 192 | let message = "Hello" 193 | <: \`\\\`a\\{b\\}c\\\`\` 194 | `); 195 | eq(res, STR('`a{b}c`')); 196 | }); 197 | 198 | test.concurrent('nested brackets', async () => { 199 | const res = await exe(` 200 | <: \`{if true {1} else {2}}\` 201 | `); 202 | eq(res, STR('1')); 203 | }); 204 | 205 | test.concurrent('new line before', async () => { 206 | const res = await exe(` 207 | <: \`{"Hello" 208 | // comment 209 | }\` 210 | `); 211 | eq(res, STR('Hello')); 212 | }); 213 | 214 | test.concurrent('new line after', async () => { 215 | const res = await exe(` 216 | <: \`{ 217 | // comment 218 | "Hello"}\` 219 | `); 220 | eq(res, STR('Hello')); 221 | }); 222 | }); 223 | 224 | -------------------------------------------------------------------------------- /test/newline.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from "vitest"; 2 | import { utils } from '../src'; 3 | import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; 4 | import { exe, getMeta, eq } from './testutils'; 5 | 6 | describe('empty lines', () => { 7 | describe('match', () => { 8 | test.concurrent('empty line', async () => { 9 | const res = await exe(` 10 | <: match 1 { 11 | // comment 12 | } 13 | `); 14 | eq(res, NULL); 15 | }); 16 | 17 | test.concurrent('empty line before case', async () => { 18 | const res = await exe(` 19 | <: match 1 { 20 | // comment 21 | case 1 => 1 22 | } 23 | `); 24 | eq(res, NUM(1)); 25 | }); 26 | 27 | test.concurrent('empty line after case', async () => { 28 | const res = await exe(` 29 | <: match 1 { 30 | case 1 => 1 31 | // comment 32 | } 33 | `); 34 | eq(res, NUM(1)); 35 | }); 36 | 37 | test.concurrent('empty line before default', async () => { 38 | const res = await exe(` 39 | <: match 1 { 40 | // comment 41 | default => 1 42 | } 43 | `); 44 | eq(res, NUM(1)); 45 | }); 46 | 47 | test.concurrent('empty line after default', async () => { 48 | const res = await exe(` 49 | <: match 1 { 50 | default => 1 51 | // comment 52 | } 53 | `); 54 | eq(res, NUM(1)); 55 | }); 56 | }); 57 | 58 | describe('call', () => { 59 | test.concurrent('empty line', async () => { 60 | const res = await exe(` 61 | @f() { 62 | 1 63 | } 64 | <:f( 65 | // comment 66 | ) 67 | `); 68 | eq(res, NUM(1)); 69 | }); 70 | 71 | test.concurrent('empty line before', async () => { 72 | const res = await exe(` 73 | @f(a) { 74 | a 75 | } 76 | <:f( 77 | // comment 78 | 1 79 | ) 80 | `); 81 | eq(res, NUM(1)); 82 | }); 83 | 84 | test.concurrent('empty line after', async () => { 85 | const res = await exe(` 86 | @f(a) { 87 | a 88 | } 89 | <:f( 90 | 1 91 | // comment 92 | ) 93 | `); 94 | eq(res, NUM(1)); 95 | }); 96 | }); 97 | 98 | describe('type params', () => { 99 | describe('function', () => { 100 | test.concurrent('empty line before', async () => { 101 | const res = await exe(` 102 | @f< 103 | // comment 104 | T 105 | >(v: T): T { 106 | v 107 | } 108 | <: f(1) 109 | `); 110 | eq(res, NUM(1)); 111 | }); 112 | 113 | test.concurrent('empty line after', async () => { 114 | const res = await exe(` 115 | @f< 116 | T 117 | // comment 118 | >(v: T): T { 119 | v 120 | } 121 | <: f(1) 122 | `); 123 | eq(res, NUM(1)); 124 | }); 125 | }); 126 | 127 | describe('function type', () => { 128 | test.concurrent('empty line before', async () => { 129 | const res = await exe(` 130 | let f: @< 131 | // comment 132 | T 133 | >(T) => T = @(v) { 134 | v 135 | } 136 | <: f(1) 137 | `); 138 | eq(res, NUM(1)); 139 | }); 140 | 141 | test.concurrent('empty line after', async () => { 142 | const res = await exe(` 143 | let f: @< 144 | T 145 | // comment 146 | >(T) => T = @(v) { 147 | v 148 | } 149 | <: f(1) 150 | `); 151 | eq(res, NUM(1)); 152 | }); 153 | }); 154 | }); 155 | 156 | describe('function params', () => { 157 | test.concurrent('empty line', async () => { 158 | const res = await exe(` 159 | @f( 160 | // comment 161 | ) { 162 | 1 163 | } 164 | <: f() 165 | `); 166 | eq(res, NUM(1)); 167 | }); 168 | 169 | test.concurrent('empty line before', async () => { 170 | const res = await exe(` 171 | @f( 172 | // comment 173 | a 174 | ) { 175 | a 176 | } 177 | <: f(1) 178 | `); 179 | eq(res, NUM(1)); 180 | }); 181 | 182 | test.concurrent('empty line after', async () => { 183 | const res = await exe(` 184 | @f( 185 | a 186 | // comment 187 | ) { 188 | a 189 | } 190 | <: f(1) 191 | `); 192 | eq(res, NUM(1)); 193 | }); 194 | }); 195 | 196 | describe('if', () => { 197 | test.concurrent('empty line between if ~ elif', async () => { 198 | const res = await exe(` 199 | <: if true { 200 | 1 201 | } 202 | // comment 203 | elif true { 204 | 2 205 | } 206 | `); 207 | eq(res, NUM(1)); 208 | }); 209 | 210 | test.concurrent('empty line between if ~ elif ~ elif', async () => { 211 | const res = await exe(` 212 | <: if true { 213 | 1 214 | } 215 | // comment 216 | elif true { 217 | 2 218 | } 219 | // comment 220 | elif true { 221 | 3 222 | } 223 | `); 224 | eq(res, NUM(1)); 225 | }); 226 | 227 | test.concurrent('empty line between if ~ else', async () => { 228 | const res = await exe(` 229 | <: if true { 230 | 1 231 | } 232 | // comment 233 | else { 234 | 2 235 | } 236 | `); 237 | eq(res, NUM(1)); 238 | }); 239 | 240 | test.concurrent('empty line between if ~ elif ~ else', async () => { 241 | const res = await exe(` 242 | <: if true { 243 | 1 244 | } 245 | // comment 246 | elif true { 247 | 2 248 | } 249 | // comment 250 | else { 251 | 3 252 | } 253 | `); 254 | eq(res, NUM(1)); 255 | }); 256 | }); 257 | 258 | describe('unary operation', () => { 259 | test.concurrent('empty line after', async () => { 260 | const res = await exe(` 261 | ! \\ 262 | // comment 263 | true 264 | `); 265 | eq(res, BOOL(false)); 266 | }); 267 | }); 268 | 269 | describe('binary operation', () => { 270 | test.concurrent('empty line before', async () => { 271 | const res = await exe(` 272 | <: 2 \\ 273 | // comment 274 | * 3 275 | `); 276 | eq(res, NUM(6)); 277 | }); 278 | }); 279 | 280 | describe('binary operation', () => { 281 | test.concurrent('empty line after', async () => { 282 | const res = await exe(` 283 | <: 2 * \\ 284 | // comment 285 | 3 286 | `); 287 | eq(res, NUM(6)); 288 | }); 289 | }); 290 | 291 | describe('variable definition', () => { 292 | test.concurrent('empty line after equal', async () => { 293 | const res = await exe(` 294 | let a = 295 | // comment 296 | 1 297 | <: a 298 | `); 299 | eq(res, NUM(1)); 300 | }); 301 | }); 302 | 303 | describe('attribute', () => { 304 | test.concurrent('empty line after', async () => { 305 | const res = await exe(` 306 | #[abc] 307 | // comment 308 | let a = 1 309 | <: a 310 | `); 311 | eq(res, NUM(1)); 312 | }); 313 | }); 314 | 315 | describe('obj literal', () => { 316 | test.concurrent('empty line', async () => { 317 | const res = await exe(` 318 | <: { 319 | // comment 320 | } 321 | `); 322 | eq(res, OBJ(new Map())); 323 | }); 324 | 325 | test.concurrent('empty line before', async () => { 326 | const res = await exe(` 327 | let x = { 328 | // comment 329 | a: 1 330 | } 331 | <: x.a 332 | `); 333 | eq(res, NUM(1)); 334 | }); 335 | 336 | test.concurrent('empty line after', async () => { 337 | const res = await exe(` 338 | let x = { 339 | a: 1 340 | // comment 341 | } 342 | <: x.a 343 | `); 344 | eq(res, NUM(1)); 345 | }); 346 | }); 347 | 348 | describe('arr literal', () => { 349 | test.concurrent('empty line', async () => { 350 | const res = await exe(` 351 | <: [ 352 | // comment 353 | ] 354 | `); 355 | eq(res, ARR([])); 356 | }); 357 | 358 | test.concurrent('empty line before', async () => { 359 | const res = await exe(` 360 | let x = [ 361 | // comment 362 | 1 363 | ] 364 | <: x[0] 365 | `); 366 | eq(res, NUM(1)); 367 | }); 368 | 369 | test.concurrent('empty line after', async () => { 370 | const res = await exe(` 371 | let x = [ 372 | 1 373 | // comment 374 | ] 375 | <: x[0] 376 | `); 377 | eq(res, NUM(1)); 378 | }); 379 | }); 380 | }); 381 | -------------------------------------------------------------------------------- /test/parser.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { describe, test } from 'vitest'; 3 | import { Scanner } from '../src/parser/scanner'; 4 | import { TOKEN, TokenKind, TokenPosition } from '../src/parser/token'; 5 | import { CharStream } from '../src/parser/streams/char-stream'; 6 | 7 | describe('CharStream', () => { 8 | test.concurrent('char', async () => { 9 | const source = 'abc'; 10 | const stream = new CharStream(source); 11 | assert.strictEqual('a', stream.char); 12 | }); 13 | 14 | test.concurrent('next', async () => { 15 | const source = 'abc'; 16 | const stream = new CharStream(source); 17 | stream.next(); 18 | assert.strictEqual('b', stream.char); 19 | }); 20 | 21 | describe('prev', () => { 22 | test.concurrent('move', async () => { 23 | const source = 'abc'; 24 | const stream = new CharStream(source); 25 | stream.next(); 26 | assert.strictEqual('b', stream.char); 27 | stream.prev(); 28 | assert.strictEqual('a', stream.char); 29 | }); 30 | 31 | test.concurrent('境界外には移動しない', async () => { 32 | const source = 'abc'; 33 | const stream = new CharStream(source); 34 | stream.prev(); 35 | assert.strictEqual('a', stream.char); 36 | }); 37 | }); 38 | 39 | test.concurrent('eof', async () => { 40 | const source = 'abc'; 41 | const stream = new CharStream(source); 42 | assert.strictEqual(false, stream.eof); 43 | stream.next(); 44 | assert.strictEqual(false, stream.eof); 45 | stream.next(); 46 | assert.strictEqual(false, stream.eof); 47 | stream.next(); 48 | assert.strictEqual(true, stream.eof); 49 | }); 50 | 51 | test.concurrent('EOFでcharを参照するとエラー', async () => { 52 | const source = ''; 53 | const stream = new CharStream(source); 54 | assert.strictEqual(true, stream.eof); 55 | try { 56 | stream.char; 57 | } catch (e) { 58 | return; 59 | } 60 | assert.fail(); 61 | }); 62 | 63 | test.concurrent('CRは読み飛ばされる', async () => { 64 | const source = 'a\r\nb'; 65 | const stream = new CharStream(source); 66 | assert.strictEqual('a', stream.char); 67 | stream.next(); 68 | assert.strictEqual('\n', stream.char); 69 | stream.next(); 70 | assert.strictEqual('b', stream.char); 71 | stream.next(); 72 | assert.strictEqual(true, stream.eof); 73 | }); 74 | }); 75 | 76 | describe('Scanner', () => { 77 | function init(source: string) { 78 | const stream = new Scanner(source); 79 | return stream; 80 | } 81 | function next(stream: Scanner, kind: TokenKind, pos: TokenPosition, opts: { hasLeftSpacing?: boolean, value?: string }) { 82 | assert.deepStrictEqual(stream.getToken(), TOKEN(kind, pos, opts)); 83 | stream.next(); 84 | } 85 | 86 | test.concurrent('eof', async () => { 87 | const source = ''; 88 | const stream = init(source); 89 | next(stream, TokenKind.EOF, { line: 1, column: 1 }, { }); 90 | next(stream, TokenKind.EOF, { line: 1, column: 1 }, { }); 91 | }); 92 | test.concurrent('keyword', async () => { 93 | const source = 'if'; 94 | const stream = init(source); 95 | next(stream, TokenKind.IfKeyword, { line: 1, column: 1 }, { }); 96 | next(stream, TokenKind.EOF, { line: 1, column: 3 }, { }); 97 | }); 98 | test.concurrent('identifier', async () => { 99 | const source = 'xyz'; 100 | const stream = init(source); 101 | next(stream, TokenKind.Identifier, { line: 1, column: 1 }, { value: 'xyz' }); 102 | next(stream, TokenKind.EOF, { line: 1, column: 4 }, { }); 103 | }); 104 | test.concurrent('invalid token', async () => { 105 | const source = '$'; 106 | try { 107 | const stream = new Scanner(source); 108 | } catch (e) { 109 | return; 110 | } 111 | assert.fail(); 112 | }); 113 | test.concurrent('words', async () => { 114 | const source = 'abc xyz'; 115 | const stream = init(source); 116 | next(stream, TokenKind.Identifier, { line: 1, column: 1 }, { value: 'abc' }); 117 | next(stream, TokenKind.Identifier, { line: 1, column: 5 }, { hasLeftSpacing: true, value: 'xyz' }); 118 | next(stream, TokenKind.EOF, { line: 1, column: 8 }, { }); 119 | }); 120 | test.concurrent('stream', async () => { 121 | const source = '@abc() { }'; 122 | const stream = init(source); 123 | next(stream, TokenKind.At, { line: 1, column: 1 }, { }); 124 | next(stream, TokenKind.Identifier, { line: 1, column: 2 }, { value: 'abc' }); 125 | next(stream, TokenKind.OpenParen, { line: 1, column: 5 }, { }); 126 | next(stream, TokenKind.CloseParen, { line: 1, column: 6 }, { }); 127 | next(stream, TokenKind.OpenBrace, { line: 1, column: 8 }, { hasLeftSpacing: true }); 128 | next(stream, TokenKind.CloseBrace, { line: 1, column: 10 }, { hasLeftSpacing: true }); 129 | next(stream, TokenKind.EOF, { line: 1, column: 11 }, { }); 130 | }); 131 | test.concurrent('multi-lines', async () => { 132 | const source = 'aaa\nbbb'; 133 | const stream = init(source); 134 | next(stream, TokenKind.Identifier, { line: 1, column: 1 }, { value: 'aaa' }); 135 | next(stream, TokenKind.NewLine, { line: 1, column: 4 }, { }); 136 | next(stream, TokenKind.Identifier, { line: 2, column: 1 }, { value: 'bbb' }); 137 | next(stream, TokenKind.EOF, { line: 2, column: 4 }, { }); 138 | }); 139 | test.concurrent('lookahead', async () => { 140 | const source = '@abc() { }'; 141 | const stream = init(source); 142 | assert.deepStrictEqual(stream.lookahead(1), TOKEN(TokenKind.Identifier, { line: 1, column: 2 }, { value: 'abc' })); 143 | next(stream, TokenKind.At, { line: 1, column: 1 }, { }); 144 | next(stream, TokenKind.Identifier, { line: 1, column: 2 }, { value: 'abc' }); 145 | next(stream, TokenKind.OpenParen, { line: 1, column: 5 }, { }); 146 | }); 147 | test.concurrent('empty lines', async () => { 148 | const source = "match 1{\n// comment\n}"; 149 | const stream = init(source); 150 | next(stream, TokenKind.MatchKeyword, { line: 1, column: 1 }, { }); 151 | next(stream, TokenKind.NumberLiteral, { line: 1, column: 7 }, { hasLeftSpacing: true, value: '1' }); 152 | next(stream, TokenKind.OpenBrace, { line: 1, column: 8 }, { }); 153 | next(stream, TokenKind.NewLine, { line: 1, column: 9 }, { }); 154 | next(stream, TokenKind.CloseBrace, { line: 3, column: 1 }, { }); 155 | next(stream, TokenKind.EOF, { line: 3, column: 2 }, { }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/testutils.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { Parser, Interpreter } from '../src'; 3 | 4 | export async function exe(script: string): Promise { 5 | const parser = new Parser(); 6 | let result = undefined; 7 | const interpreter = new Interpreter({}, { 8 | out(value) { 9 | if (!result) result = value; 10 | else if (!Array.isArray(result)) result = [result, value]; 11 | else result.push(value); 12 | }, 13 | log(type, {val}) { 14 | if (type === 'end') result ??= val; 15 | }, 16 | maxStep: 9999, 17 | }); 18 | const ast = parser.parse(script); 19 | await interpreter.exec(ast); 20 | return result; 21 | }; 22 | 23 | export const getMeta = (script: string) => { 24 | const parser = new Parser(); 25 | const ast = parser.parse(script); 26 | 27 | const metadata = Interpreter.collectMetadata(ast); 28 | 29 | return metadata; 30 | }; 31 | 32 | export const eq = (a, b) => { 33 | assert.deepEqual(a.type, b.type); 34 | assert.deepEqual(a.value, b.value); 35 | }; 36 | 37 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { describe, test } from 'vitest'; 3 | import { utils } from '../src'; 4 | import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; 5 | import { AiScriptRuntimeError } from '../src/error'; 6 | import { exe, getMeta, eq } from './testutils'; 7 | 8 | describe('function types', () => { 9 | test.concurrent('multiple params', async () => { 10 | const res = await exe(` 11 | let f: @(str, num) => bool = @() { true } 12 | <: f('abc', 123) 13 | `); 14 | eq(res, TRUE); 15 | }); 16 | }); 17 | 18 | describe('generics', () => { 19 | describe('function', () => { 20 | test.concurrent('expr', async () => { 21 | const res = await exe(` 22 | let f = @(v: T): void {} 23 | <: f("a") 24 | `); 25 | eq(res, NULL); 26 | }); 27 | 28 | test.concurrent('consumer', async () => { 29 | const res = await exe(` 30 | @f(v: T): void {} 31 | <: f("a") 32 | `); 33 | eq(res, NULL); 34 | }); 35 | 36 | test.concurrent('identity function', async () => { 37 | const res = await exe(` 38 | @f(v: T): T { v } 39 | <: f(1) 40 | `); 41 | eq(res, NUM(1)); 42 | }); 43 | 44 | test.concurrent('use as inner type', async () => { 45 | const res = await exe(` 46 | @vals(v: obj): arr { 47 | Obj:vals(v) 48 | } 49 | <: vals({ a: 1, b: 2, c: 3 }) 50 | `); 51 | eq(res, ARR([NUM(1), NUM(2), NUM(3)])); 52 | }); 53 | 54 | test.concurrent('use as variable type', async () => { 55 | const res = await exe(` 56 | @f(v: T): void { 57 | let v2: T = v 58 | } 59 | <: f(1) 60 | `); 61 | eq(res, NULL); 62 | }); 63 | 64 | test.concurrent('use as function type', async () => { 65 | const res = await exe(` 66 | @f(v: T): @() => T { 67 | let g: @() => T = @() { v } 68 | g 69 | } 70 | <: f(1)() 71 | `); 72 | eq(res, NUM(1)) 73 | }); 74 | 75 | test.concurrent('curried', async () => { 76 | const res = await exe(` 77 | @concat(a: A): @(B) => str { 78 | @(b: B) { 79 | \`{a}{b}\` 80 | } 81 | } 82 | <: concat("abc")(123) 83 | `); 84 | eq(res, STR('abc123')); 85 | }); 86 | 87 | test.concurrent('new lines', async () => { 88 | const res = await exe(` 89 | @f< 90 | T 91 | U 92 | >(x: T, y: U): arr { 93 | [x, y] 94 | } 95 | <: f("abc", 123) 96 | `); 97 | eq(res, ARR([STR('abc'), NUM(123)])); 98 | }); 99 | 100 | test.concurrent('duplicate', async () => { 101 | await assert.rejects(() => exe(` 102 | @f(v: T) {} 103 | `)); 104 | }); 105 | 106 | test.concurrent('empty', async () => { 107 | await assert.rejects(() => exe(` 108 | @f<>() {} 109 | `)); 110 | }); 111 | }); 112 | }); 113 | 114 | describe('union', () => { 115 | test.concurrent('variable type', async () => { 116 | const res = await exe(` 117 | let a: num | null = null 118 | <: a 119 | `); 120 | eq(res, NULL); 121 | }); 122 | 123 | test.concurrent('more inners', async () => { 124 | const res = await exe(` 125 | let a: str | num | null = null 126 | <: a 127 | `); 128 | eq(res, NULL); 129 | }); 130 | 131 | test.concurrent('inner type', async () => { 132 | const res = await exe(` 133 | let a: arr = ["abc", 123] 134 | <: a 135 | `); 136 | eq(res, ARR([STR('abc'), NUM(123)])); 137 | }); 138 | 139 | test.concurrent('param type', async () => { 140 | const res = await exe(` 141 | @f(x: num | str): str { 142 | \`{x}\` 143 | } 144 | <: f(1) 145 | `); 146 | eq(res, STR('1')); 147 | }); 148 | 149 | test.concurrent('return type', async () => { 150 | const res = await exe(` 151 | @f(): num | str { 1 } 152 | <: f() 153 | `); 154 | eq(res, NUM(1)); 155 | }); 156 | 157 | test.concurrent('type parameter', async () => { 158 | const res = await exe(` 159 | @f(v: T): T | null { null } 160 | <: f(1) 161 | `); 162 | eq(res, NULL); 163 | }); 164 | 165 | test.concurrent('function type', async () => { 166 | const res = await exe(` 167 | let f: @(num | str) => str = @(x) { \`{x}\` } 168 | <: f(1) 169 | `); 170 | eq(res, STR('1')); 171 | }); 172 | 173 | test.concurrent('invalid inner', async () => { 174 | await assert.rejects(() => exe(` 175 | let a: ThisIsAnInvalidTypeName | null = null 176 | `)); 177 | }); 178 | }); 179 | 180 | describe('simple', () => { 181 | test.concurrent('error', async () => { 182 | const res = await exe(` 183 | let a: error = Error:create("Ai") 184 | <: a 185 | `); 186 | eq(res, ERROR('Ai')); 187 | }); 188 | 189 | test.concurrent('never', async () => { 190 | const res = await exe(` 191 | @f() { 192 | let a: never = eval { 193 | return 1 194 | } 195 | } 196 | <: f() 197 | `); 198 | eq(res, NUM(1)); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /translations/en/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.19.0 2 | - Fix: Zero passed to `Date:year` family functions ignored 3 | - Fix location informations in syntax errors and others 4 | - `arr.reduce` now throws a distinct error when called with an empty array and no second argument 5 | - Add `str.pad_start`,`str.pad_end` 6 | - Add `arr.insert`,`arr.remove` 7 | - asynchronization of `arr.sort` for better processing speed 8 | - Add `arr.flat`, `arr.flat_map` 9 | - Add `Uri:encode_full`, `Uri:encode_component`, `Uri:decode_full`, `Uri:decode_component` 10 | - Add `str.starts_with`,`str.ends_with` 11 | - Add `arr.splice` 12 | - Add `arr.at` 13 | - For Hosts: With an error handler, Interpreter now do the abortion of all processes only when `abortOnError` option is set to true 14 | 15 | # 0.18.0 16 | - New function `Core:abort` for immediate abortion of the program 17 | - Add array version of `index_of` 18 | - `str.index_of` and `arr.index_of` now accept a second argument 19 | - Remove type restriction on the argument for `arr.incl` 20 | - Add `Date:millisecond` 21 | - Add `arr.fill`, `arr.repeat`, `Arr:create` 22 | - Destructuring assignment available, like JavaScript(but limited functionality now) 23 | - Amend the error message on duplicated declaration of variable 24 | - Variables under nested namespaces are now accessible 25 | - Add `arr.every`, `arr.some` 26 | - Add `Date:to_iso_str` 27 | 28 | # 0.17.0 29 | - Fix `package.json` 30 | - New function `Error:create` to create a error-type value 31 | - New function `Obj:merge` to get a merge of two objects 32 | - Fix: Chainings(`[]` for index access, `.` for property access, `()` for function call) used in conjunction with parentheses may cause unexpected behaviour 33 | - New functions: `Str#charcode_at` `Str#to_arr` `Str#to_char_arr` `Str#to_charcode_arr` `Str#to_utf8_byte_arr` `Str#to_unicode_codepoint_arr` `Str:from_unicode_codepoints` `Str:from_utf8_bytes` 34 | - Fix: `Str#codepoint_at` not supporting surrogate pairs 35 | - IndexOutOfRangeError now also occurs when assigning non-integer index or outside boundaries of arrays 36 | ## Note 37 | CHANGELOG had a missing record in V0.16.0. 38 | >- Add new functions `Str:from_codepoint` `Str#codepoint_at` 39 | 40 | # 0.16.0 41 | - **Namespaces can no longer include `var` (while `let` is available as it has been)** 42 | - `Core:to_str` and template syntax can now convert any type of values into string 43 | - Add `Core:sleep`: waits specified milliseconds 44 | - New syntax: `exists ` can judge specified name of variable actually exists in runtime 45 | - Object values can be now referenced via index (ex. `object['index']`) 46 | - New value type: Error Type (type name: `error`) 47 | - `Json:parse` now returns error-type value when parsing failed 48 | - Fix: Variables defined with `let` is yet mutable 49 | - Add new functions `Str:from_codepoint` `Str#codepoint_at` 50 | 51 | ## For Hosts 52 | - **Breaking Change** Subclasses of AiScriptError now have `AiScript-` prefix (ex. SyntaxError→AiScriptSyntaxError) 53 | - `Interpreter`'s second constructor argument now accepts `err` option: pass a callback function to be called when `Interpreter.exec` fails **or `Async:interval`/`Async:timeout` fails**. Noted that using this feature disables AiScript process from throwing JavaScript errors. 54 | - Native functions can use `opts.topCall` instead of `opts.call` to call error callback when failing like two functions above. **Use only when necessary. If `opts.call` calls error callback correctly, use that.** 55 | 56 | # 0.15.0 57 | - Enrichment of `Math:` 58 | - Fix: Terms of operator `&&`, `||` may not be converted correctly 59 | 60 | # 0.14.1 61 | - Fix: Short-circuit evaluation of `&&`, `||` is not working correctly 62 | - Fix: operator `+=`, `-=` may overwrite unrelated variable 63 | 64 | # 0.14.0 65 | - Add `Obj:vals` that returns an array of values of given object 66 | - Add `Json:parsable` that judges whether given string is parsable with `Json:parse` 67 | - When first argument of or/and determines the result, now second argument is no longer evaluated 68 | - Fix immediate value check in Async:interval 69 | 70 | # 0.13.3 71 | - Random number generation now includes specified maximum value in returning range 72 | 73 | # 0.13.2 74 | - `Date:year`,`Date:month`,`Date:day`,`Date:hour`,`Date:minute`,`Date:second` are now accepting a number argument to specify the time 75 | - Add `array.sort` and comparison functions `Str:lt`, `Str:gt` 76 | - Random number generation now includes specified maximum value in returning range 77 | 78 | # 0.13.1 79 | - Fix: `Json:stringify` returns corrupted value when given functions as argument 80 | 81 | # 0.13.0 82 | - Index arguments for callbacks of some array properties are now zero-start: `map`,`filter`,`reduce`,`find` 83 | - Add `@Math:ceil(x: num): num` 84 | - Exponentiation function `Core:pow` and syntax sugar `^` 85 | - Minor fixes of parsing 86 | 87 | # 0.12.4 88 | - block comment `/* ... */` 89 | - Math:Infinity 90 | 91 | # 0.12.3 92 | - Fix: `break`/`return` does not work in `each` statement 93 | - IndexOutOfRangeError now occurs when accessing outside boundaries of arrays 94 | 95 | # 0.12.2 96 | - Logical Not operator `!` 97 | - Adjustment of interpreter processing speed 98 | 99 | # 0.12.1 100 | - Single quotes are now available for string literal 101 | - Fix: `return` does not work in for/loop statement 102 | - Runtime now constantly pauses a few milliseconds to prevent infinite loops from causing runtime freezes 103 | 104 | # 0.12.0 105 | ## Breaking changes 106 | - `#` for variable definition → `let` 107 | - `$` for variable definition → `var` 108 | - `<-` for assignment → `=` 109 | - `=` for comparison → `==` 110 | - `&` → `&&` 111 | - `|` → `||` 112 | - `? ~ .? ~ .` → `if ~ elif ~ else` 113 | - `? x { 42 => yes }` → `match x { 42 => yes }` 114 | - `_` → `null` 115 | - `<<` → `return` 116 | - `~` → `for` 117 | - `~~` → `each` 118 | - `+ attributeName attributeValue` → `#[attributeName attributeValue]` 119 | - Removed `+`/`-` notation for truth values. 120 | - for and each no longer return an array 121 | - Block statement `{ }` → `eval { }` 122 | - Arrays are now indexed from zero 123 | - Some std methods are now written in property-like style: 124 | - `Str:to_num("123")` -> `"123".to_num()` 125 | - `Arr:len([1 2 3])` -> `[1 2 3].len` 126 | - etc 127 | 128 | ## Features 129 | - `continue` 130 | - `break` 131 | - `loop` 132 | 133 | ## Fixes 134 | - Fixed an issue where empty functions could not be defined. 135 | - Fixed an issue where empty scripts were not allowed. 136 | - Fixed increment and decrement of namespaced variables. 137 | - Fixed an issue that prevented assignment to variables with namespaces. 138 | -------------------------------------------------------------------------------- /translations/en/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | Thank you for your interest in our project! 3 | This document contains all the information you need to contribute to the project. 4 | 5 | ## Before implementing 6 | When you want to add a feature or fix a bug, first have the design and policy reviewed in an Issue or something similar (if it is not there, please create one). Without this step, there is a high possibility that the PR will not be merged even if you have implemented it. 7 | 8 | Also, when you start implementation, please assign yourself to the issue in question (if you cannot do it yourself, please ask another member to assign you). 9 | By indicating that you are going to implement it, you can avoid conflicts. 10 | 11 | If your request has changes of language behavior, please update the tests and [documents](https://github.com/aiscript-dev/aiscript-dev.github.io) simultaneously. 12 | 13 | ## Tools 14 | ### Vitest 15 | In this project, we have introduced [Vitest](https://vitest.dev) as a testing framework. 16 | Tests are placed in the [`/test` directory](./test). 17 | 18 | Testing is done automatically by CI for each commit/each PR. 19 | To run the test in your local environment, run `npm run test`. 20 | 21 | ### API Extractor 22 | In this project, we have introduced [API Extractor](https://api-extractor.com/), which is responsible for generating API reports. 23 | The API report is a snapshot of the API, so to speak, and contains the definitions of the various functions and types that the library exposes (exports) to the outside world. When you run the `npm run api` command, the current report will be generated in the [`/etc` directory](./etc). 24 | 25 | When there is a change in the API being exported, the content of the report generated will naturally change, so for example, by comparing the report generated in the develop branch with the report generated in the PR branch, you can use it to detect unintended destructive changes or to check the impact of destructive changes. 26 | Also, inside CI, which is executed for each commit and each PR, API reports are generated each time to check if there are any differences with the existing reports. If there are any differences, an error will occur. 27 | 28 | When you create a PR, please run the `npm run api` command to generate API reports and commit any differences. 29 | Committing the report will show that the disruptive change was intended, and as mentioned above, the difference between the reports will make it easier to review the scope of the impact. 30 | 31 | ### Codecov 32 | In this project, we have introduced [Codecov](https://about.codecov.io/) to measure coverage. Coverage is a measure of how much of the code is covered by the tests. 33 | 34 | The coverage measurement is done automatically by CI and no special operation is required. The coverage can be viewed from [here](https://codecov.io/gh/syuilo/aiscript). 35 | 36 | Also, for each PR, the coverage of that branch is automatically calculated, and the Codecov bot comments a report containing the difference between the coverage of the branch and the coverage of the merged branch. This allows you to see how much coverage is increased/decreased by merging the PRs. 37 | 38 | ## Tips for Reviewers 39 | You can read a lot about it in the template for creating a PR. (Maybe you can move it to this document?). 40 | Also, be aware of the "review perspective" described below. 41 | 42 | ## Tips for Reviewers 43 | - Be willing to comment on the good points as well as the things you want fixed. 44 | - It will motivate you to contribute. 45 | 46 | ### Review perspective 47 | - security 48 | - Doesn't merging this PR create a vulnerability? 49 | - Performance 50 | - Will merging these PRs cause unexpected performance degradation? 51 | - Is there a more efficient way? 52 | - Test 53 | - Is the expected behavior ensured by the test? 54 | - Are there any loose ends or leaks? 55 | - Are you able to check for abnormal systems? 56 | 57 | -------------------------------------------------------------------------------- /translations/en/README.md: -------------------------------------------------------------------------------- 1 |

AiScript

2 | 3 | [![](https://img.shields.io/npm/v/@syuilo/aiscript.svg?style=flat-square)](https://www.npmjs.com/package/@syuilo/aiscript) 4 | ![](https://github.com/syuilo/aiscript/workflows/ci/badge.svg) 5 | [![codecov](https://codecov.io/gh/syuilo/aiscript/branch/master/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/syuilo/aiscript) 6 | [![](https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square)](http://opensource.org/licenses/MIT) 7 | [![](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&logo=github)](http://makeapullrequest.com) 8 | 9 | > **AiScript** is a scripting language that runs on JavaScript. Not altJS. 10 | 11 | [Play online ▶](https://aiscript-dev.github.io/aiscript/) 12 | 13 | AiScript is a multi-paradigm programming language that runs on JavaScript, not AltJS (1). 14 | 15 | * First-class support for arrays, objects, functions, etc. 16 | * Flexibility, including conditional branches and blocks can be treated as expressions 17 | * Easy to write, no need for semicolons or commas 18 | * Runs in a secure(2) sandbox environment 19 | * Variables and functions can be provided from the host 20 | 21 | > (1) ... It "runs on" JavaScript, not is "converted" to JavaScript. Therefore, it is not AltJS. 22 | 23 | > (2) ... Not being able to access the host's information. 24 | 25 | This repository contains parsers and processors implemented in JavaScript. 26 | 27 | Note: AiScript and [Misskey](https://github.com/syuilo/misskey) are completely independent projects. AiScript does not prescribe any specific host, but Misskey is the largest user of AiScript (today!) 28 | 29 | ## Getting started (language) 30 | [See here](https://aiscript-dev.github.io/en/guides/get-started.html) 31 | 32 | ## Getting started (host implementation) 33 | [See here](https://aiscript-dev.github.io/en/guides/implementation.html) 34 | 35 | ## Example programs 36 | ### Hello world 37 | ``` 38 | <: "Hello, world!" 39 | ``` 40 | 41 | ### Fizz Buzz 42 | ``` 43 | for (#i, 100) { 44 | <: if (i % 15 == 0) "FizzBuzz" 45 | elif (i % 3 == 0) "Fizz" 46 | elif (i % 5 == 0) "Buzz" 47 | else i 48 | } 49 | ``` 50 | 51 | ## License 52 | [MIT](LICENSE) 53 | -------------------------------------------------------------------------------- /translations/en/docs/README.md: -------------------------------------------------------------------------------- 1 | # Documents moved 2 | The documents that were under this directory have been moved to the new web page. 3 | 4 | [issue](https://github.com/aiscript-dev/aiscript/issues/804) 5 | 6 | [New Page](https://aiscript-dev.github.io/en/) 7 | 8 | [Repository of the New Page](https://github.com/aiscript-dev/aiscript-dev.github.io) 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "noEmitOnError": false, 5 | "noImplicitAny": true, 6 | "noImplicitReturns": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "noUncheckedIndexedAccess": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "target": "es2022", 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext", 14 | "esModuleInterop": true, 15 | "allowJs": true, 16 | "removeComments": false, 17 | "resolveJsonModule": true, 18 | "rootDir": "./src", 19 | "noLib": false, 20 | "outDir": "built", 21 | "strictNullChecks": true 22 | }, 23 | "compileOnSave": false, 24 | "include": [ 25 | "./src/**/*" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /unreleased/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiscript-dev/aiscript/f6670a79cf4946b927950aac39c0bc5edfb98994/unreleased/.gitkeep -------------------------------------------------------------------------------- /unreleased/IEEE 754 compliance around NaN.md: -------------------------------------------------------------------------------- 1 | - 関数`Core:pow`、`Core:div`、`Math:sqrt`が例外を発生する問題を修正。 2 | - `0 / 0`、`-1 ^ 0.5`、`Math:sqrt(-1)`が`NaN`を返すようになります。 3 | - `NaN`は`v != v`により検出できます。 4 | -------------------------------------------------------------------------------- /unreleased/accept-multi-new-line.md: -------------------------------------------------------------------------------- 1 | - 複数の改行が1つの改行と同等に扱われるようになりました。次に示す部分で複数の改行を挿入できます。 2 | - 空のmatch式の波括弧内および、match式の各case節やdefault節の前後 3 | - 引数のない関数呼び出しの丸括弧内および、関数呼び出しの各引数の前後 4 | - 引数のない関数定義の丸括弧内および、関数定義の各引数の前後 5 | - if式において、then節やelif節、else節の間 6 | - 単項演算や二項演算において、バックスラッシュの後 7 | - 変数定義において、等号と式の間 8 | - 属性と文の間 9 | -------------------------------------------------------------------------------- /unreleased/assign-left-eval-once.md: -------------------------------------------------------------------------------- 1 | - **Breaking Change** 複合代入文(`+=`, `-=`)の左辺が1回だけ評価されるようになりました。 2 | -------------------------------------------------------------------------------- /unreleased/attr-under-ns.md: -------------------------------------------------------------------------------- 1 | - 名前空間下の変数定義に属性を付与できるようになりました。 2 | -------------------------------------------------------------------------------- /unreleased/braces-in-template-expression.md: -------------------------------------------------------------------------------- 1 | - テンプレートリテラルに波括弧を含む式を埋め込むことができるようになりました。 2 | -------------------------------------------------------------------------------- /unreleased/callstack.md: -------------------------------------------------------------------------------- 1 | - ランタイムエラーにコールスタックの情報を追加。 2 | -------------------------------------------------------------------------------- /unreleased/date-parse-err.md: -------------------------------------------------------------------------------- 1 | - `Date:parse`がパース失敗時にエラー型の値を返すように 2 | -------------------------------------------------------------------------------- /unreleased/destr-define.md: -------------------------------------------------------------------------------- 1 | - 変数宣言(each文での宣言を含む)と関数の仮引数で分割代入ができるように(名前空間内では分割代入を使用した宣言はできません。) 2 | -------------------------------------------------------------------------------- /unreleased/irq-config.md: -------------------------------------------------------------------------------- 1 | - For Hosts: Interpreterのオプションに`irqRate`と`irqSleep`を追加 2 | - `irqRate`はInterpreterの定期休止が何ステップに一回起こるかを指定する数値 3 | - `irqSleep`は休止時間をミリ秒で指定する数値、または休止ごとにawaitされるPromiseを返す関数 4 | -------------------------------------------------------------------------------- /unreleased/jump-statements.md: -------------------------------------------------------------------------------- 1 | - return文、break文、continue文の挙動が変更されました。 2 | - Fix: eval式やif式内でreturn文あるいはbreak文、continue文を使用すると不正な値が取り出せる不具合を修正しました。 3 | - return文は関数スコープ内でないと文法エラーになります。 4 | - ラベルが省略されたbreak文およびcontinue文は反復処理文(for, each, while, do-while, loop)のスコープ内でないと文法エラーになります。 5 | - return文は常に関数から脱出します。 6 | - ラベルが省略されたbreak文は必ず最も内側の反復処理文の処理を中断し、ループから脱出します。 7 | - continue文は必ず最も内側の反復処理文の処理を中断し、ループの先頭に戻ります。 8 | - eval, if, match, loop, while, do-while, for, eachにラベルを付けてbreak文やcontinue文で指定したブロックから脱出できるようになります。eval, if, matchから脱出するbreak文には値を指定することができます。 9 | -------------------------------------------------------------------------------- /unreleased/match-sep-before-default.md: -------------------------------------------------------------------------------- 1 | - **Breaking Change** match式において、case節とdefault節の間に区切り文字が必須になりました。case節の後にdefault節を区切り文字なしで続けると文法エラーになります。 2 | -------------------------------------------------------------------------------- /unreleased/new-lines-in-template-expression.md: -------------------------------------------------------------------------------- 1 | - テンプレートリテラル内に埋め込まれた式の先頭および末尾の改行が許容されるようになりました。 2 | -------------------------------------------------------------------------------- /unreleased/next-past.md: -------------------------------------------------------------------------------- 1 | - 新しいAiScriptパーサーを実装 2 | - スペースの厳密さが緩和 3 | - **Breaking Change** 改行トークンを導入。改行の扱いが今までより厳密になりました。改行することができる部分以外では文法エラーになります。 4 | - 文字列リテラルやテンプレートで、`\`とそれに続く1文字は全てエスケープシーケンスとして扱われるように 5 | - 文法エラーの表示を改善。理由を詳細に表示するように。 6 | - 複数行のコメントがある時に文法エラーの表示行数がずれる問題を解消しました。 7 | - 実行時エラーの発生位置が表示されるように。 8 | - **Breaking Change** パースの都合によりmatch文の構文を変更。パターンの前に`case`キーワードが必要となり、`*`は`default`に変更。 9 | - **Breaking Change** 多くの予約語を追加。これまで変数名等に使えていた名前に影響が出る可能性があります。 10 | - **Breaking Change** 配列及び関数の引数において、空白区切りが使用できなくなりました。`,`または改行が必要です。 11 | - **Breaking Change** 関数同士の比較の実装 12 | - **Breaking Change** `+`や`!`などの演算子の優先順位に変更があります。新しい順序は[syntax.md](docs/syntax.md#%E6%BC%94%E7%AE%97%E5%AD%90)を参照して下さい。 13 | - **Breaking Change** 組み込み関数`Num:to_hex`は組み込みプロパティ`num#to_hex`に移動しました。 14 | - **Breaking Change** `arr.sort`を安定ソートに変更 15 | -------------------------------------------------------------------------------- /unreleased/obj-extract: -------------------------------------------------------------------------------- 1 | - 関数`Obj:pick`を追加 2 | -------------------------------------------------------------------------------- /unreleased/object-key-string.md: -------------------------------------------------------------------------------- 1 | - オブジェクトリテラルのキーに文字列リテラルを記述できるようになりました。 2 | -------------------------------------------------------------------------------- /unreleased/optional-args.md: -------------------------------------------------------------------------------- 1 | - 省略可能引数と初期値付き引数を追加。引数名に`?`を後置することでその引数は省略可能となります。引数に`=<式>`を後置すると引数に初期値を設定できます。省略可能引数は初期値`null`の引数と同等です。 2 | - BREAKING: いずれでもない引数が省略されると即時エラーとなるようになりました。 3 | -------------------------------------------------------------------------------- /unreleased/pause.md: -------------------------------------------------------------------------------- 1 | - For Hosts: `interpreter.pause()`で実行の一時停止ができるように 2 | - `interpreter.unpause()`で再開 3 | - 再開後に`Async:`系の待ち時間がリセットされる不具合がありますが、修正の目処は立っていません 4 | -------------------------------------------------------------------------------- /unreleased/random algorithms.md: -------------------------------------------------------------------------------- 1 | - 関数`Math:gen_rng`に第二引数`algorithm`をオプション引数として追加。 2 | - アルゴリズムを`chacha20`、`rc4`、`rc4_legacy`から選べるようになりました。 3 | - **Breaking Change** `algorithm`を指定しない場合、`chacha20`が選択されます。 4 | - Fix: **Breaking Change** `Math:rnd`が範囲外の値を返す可能性があるのをアルゴリズムの変更により修正。 5 | -------------------------------------------------------------------------------- /unreleased/rename-arg-param.md: -------------------------------------------------------------------------------- 1 | - For Hosts: いくつかの型で args を params にリネームしました。 2 | -------------------------------------------------------------------------------- /unreleased/script-file.md: -------------------------------------------------------------------------------- 1 | - AiScriptのスクリプトファイルを表す拡張子が`.is`から`.ais`に変更されました。 2 | - ファイル実行機能により読み取られるファイルの名前が`test.is`から`main.ais`へ変更されました。 3 | -------------------------------------------------------------------------------- /unreleased/single-statement-clause-scope.md: -------------------------------------------------------------------------------- 1 | - **Breaking Change** if式やmatch式、for文の内容が1つの文である場合にもスコープが生成されるようになりました。これらの構文内で定義された変数は外部から参照できなくなります。 2 | -------------------------------------------------------------------------------- /unreleased/strictify-types.md: -------------------------------------------------------------------------------- 1 | - Breaking For Hosts: 曖昧な型を変更しました。(TypeScriptの型のみの変更であり、JavaScriptの値としては変更はありません) 2 | - `Interpreter`のオプションのlog関数の引数の型 3 | - `AiScriptError`のinfoの型 4 | - `Interpreter.collectMetadata`、`valToJs`の戻り値の型 5 | - `Node`型で存在しないプロパティーの削除 6 | -------------------------------------------------------------------------------- /unreleased/unary-sign-operators.md: -------------------------------------------------------------------------------- 1 | - 単項演算子の正号 `+`・負号 `-`が数値リテラル以外の式にも使用できるようになりました。 2 | -------------------------------------------------------------------------------- /unreleased/unified-type-annotation.md: -------------------------------------------------------------------------------- 1 | - 以下の型注釈ができるようになりました。 2 | - 関数宣言および関数型でのジェネリクス 3 | - ユニオン型 4 | - error型 5 | - never型 6 | -------------------------------------------------------------------------------- /unreleased/while.md: -------------------------------------------------------------------------------- 1 | - while文とdo-while文を追加 2 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | include: ['src'], 7 | }, 8 | include: ['test/**'], 9 | exclude: ['test/testutils.ts'], 10 | }, 11 | }); 12 | --------------------------------------------------------------------------------