├── .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 |

2 |
3 | [](https://www.npmjs.com/package/@syuilo/aiscript)
4 | [](https://github.com/syuilo/aiscript/actions/workflows/test.yml)
5 | [](https://codecov.io/gh/syuilo/aiscript)
6 | [](http://opensource.org/licenses/MIT)
7 | [](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 |
2 |
3 |
4 |
AiScript (v{{ AISCRIPT_VERSION }}) Playground
5 |
6 |
7 |
8 |
11 |
15 |
16 |
17 |
18 | Output
19 |
20 |
21 |
22 |
23 |
{{ log.type }} {{ log.text }}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
{{ JSON.stringify(ast, (_key, value) => {
32 | if (value instanceof Map) {
33 | return Object.fromEntries(value);
34 | }
35 | return value;
36 | }, '\t') }}
37 |
38 |
39 |
44 |
49 |
50 |
51 |
52 |
53 |
148 |
149 |
273 |
--------------------------------------------------------------------------------
/playground/src/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 | e.target === bg && $emit('exit')">
3 |
21 |
22 |
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