kokoscript 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,342 @@
1
+ // パーサー(構文解析器) - 句読点ベース
2
+
3
+ const { TOKEN_TYPES } = require('./lexer');
4
+
5
+ class ASTNode {
6
+ constructor(type, data = {}) {
7
+ this.type = type;
8
+ Object.assign(this, data);
9
+ }
10
+ }
11
+
12
+ class Parser {
13
+ constructor(tokens) {
14
+ this.tokens = tokens.filter(t => t.type !== TOKEN_TYPES.NEWLINE);
15
+ this.pos = 0;
16
+ }
17
+
18
+ parse() {
19
+ const statements = [];
20
+ while (!this.isAtEnd()) {
21
+ const stmt = this.parseStatement();
22
+ if (stmt) statements.push(stmt);
23
+ }
24
+ return new ASTNode('Program', { statements });
25
+ }
26
+
27
+ parseStatement() {
28
+ if (this.check('変数')) {
29
+ return this.parseVariableDeclaration();
30
+ } else if (this.check('もし')) {
31
+ return this.parseIfStatement();
32
+ } else if (this.check('関数')) {
33
+ return this.parseFunctionDeclaration();
34
+ } else if (this.peek().type === TOKEN_TYPES.NUMBER) {
35
+ return this.parseRepeatStatement();
36
+ } else if (this.checkIdentifier()) {
37
+ return this.parseCallOrAssignment();
38
+ } else if (this.match('返す')) {
39
+ return this.parseReturnStatement();
40
+ } else if (this.peek().type === TOKEN_TYPES.STRING) {
41
+ return this.parseExpressionStatement();
42
+ }
43
+
44
+ this.advance();
45
+ return null;
46
+ }
47
+
48
+ parseVariableDeclaration() {
49
+ // 変数、名前は値。
50
+ this.consume('変数');
51
+ this.consumeComma();
52
+ const name = this.consumeIdentifier();
53
+ this.consume('は');
54
+ const value = this.parseExpression();
55
+ this.consumePeriod();
56
+
57
+ return new ASTNode('VariableDeclaration', { name, value });
58
+ }
59
+
60
+ parseIfStatement() {
61
+ // もし、条件ならば、...そうでなければ、...終わり。
62
+ this.consume('もし');
63
+ this.consumeComma();
64
+ const condition = this.parseCondition();
65
+ this.consume('ならば');
66
+ this.consumeComma();
67
+
68
+ const consequent = [];
69
+ while (!this.check('そうでなければ') && !this.check('終わり') && !this.isAtEnd()) {
70
+ const stmt = this.parseStatement();
71
+ if (stmt) consequent.push(stmt);
72
+ }
73
+
74
+ let alternate = [];
75
+ if (this.match('そうでなければ')) {
76
+ this.consumeComma();
77
+ while (!this.check('終わり') && !this.isAtEnd()) {
78
+ const stmt = this.parseStatement();
79
+ if (stmt) alternate.push(stmt);
80
+ }
81
+ }
82
+
83
+ this.consume('終わり');
84
+ this.consumePeriod();
85
+
86
+ return new ASTNode('IfStatement', { condition, consequent, alternate });
87
+ }
88
+
89
+ parseCondition() {
90
+ // 値が値より大きい
91
+ const left = this.parseExpression();
92
+
93
+ if (this.match('が')) {
94
+ const right = this.parseExpression();
95
+
96
+ let operator = '==';
97
+ if (this.match('より')) {
98
+ if (this.match('大きい')) {
99
+ operator = '>';
100
+ } else if (this.match('小さい')) {
101
+ operator = '<';
102
+ }
103
+ } else if (this.match('以上')) {
104
+ operator = '>=';
105
+ } else if (this.match('以下')) {
106
+ operator = '<=';
107
+ } else if (this.match('等しい')) {
108
+ operator = '==';
109
+ } else {
110
+ // 「が」だけの場合は真偽値チェック
111
+ return left;
112
+ }
113
+
114
+ return new ASTNode('BinaryExpression', { operator, left, right });
115
+ }
116
+
117
+ return left;
118
+ }
119
+
120
+ parseRepeatStatement() {
121
+ // 3回、繰り返す、...終わり。
122
+ const count = this.consumeNumber();
123
+ this.consume('回');
124
+ this.consumeComma();
125
+ this.consume('繰り返す');
126
+ this.consumeComma();
127
+
128
+ const body = [];
129
+ while (!this.check('終わり') && !this.isAtEnd()) {
130
+ const stmt = this.parseStatement();
131
+ if (stmt) body.push(stmt);
132
+ }
133
+
134
+ this.consume('終わり');
135
+ this.consumePeriod();
136
+
137
+ return new ASTNode('RepeatStatement', { count, body });
138
+ }
139
+
140
+ parseFunctionDeclaration() {
141
+ // 関数、名前は、引数で、...終わり。
142
+ this.consume('関数');
143
+ this.consumeComma();
144
+ const name = this.consumeIdentifier();
145
+ this.consume('は');
146
+ this.consumeComma();
147
+
148
+ const params = [];
149
+ if (!this.checkPeriod() && this.checkIdentifier()) {
150
+ params.push(this.consumeIdentifier());
151
+ while (this.match('と')) {
152
+ params.push(this.consumeIdentifier());
153
+ }
154
+ this.consume('で');
155
+ this.consumeComma();
156
+ }
157
+
158
+ const body = [];
159
+ while (!this.check('終わり') && !this.isAtEnd()) {
160
+ const stmt = this.parseStatement();
161
+ if (stmt) body.push(stmt);
162
+ }
163
+
164
+ this.consume('終わり');
165
+ this.consumePeriod();
166
+
167
+ return new ASTNode('FunctionDeclaration', { name, params, body });
168
+ }
169
+
170
+ parseCallOrAssignment() {
171
+ const name = this.consumeIdentifier();
172
+
173
+ if (this.match('に')) {
174
+ // 関数呼び出し: 名前に引数を渡す。
175
+ const args = [];
176
+ args.push(this.parseExpression());
177
+
178
+ while (this.match('と')) {
179
+ args.push(this.parseExpression());
180
+ }
181
+
182
+ this.consume('を');
183
+ this.consume('渡す');
184
+ this.consumePeriod();
185
+
186
+ return new ASTNode('CallExpression', { name, args });
187
+ } else if (this.match('は')) {
188
+ // 変数代入: 名前は値。
189
+ const value = this.parseExpression();
190
+ this.consumePeriod();
191
+
192
+ return new ASTNode('Assignment', { name, value });
193
+ }
194
+
195
+ return new ASTNode('Identifier', { name });
196
+ }
197
+
198
+ parseReturnStatement() {
199
+ // 返す、値。
200
+ this.consume('返す');
201
+ this.consumeComma();
202
+ const value = this.parseExpression();
203
+ this.consumePeriod();
204
+
205
+ return new ASTNode('ReturnStatement', { value });
206
+ }
207
+
208
+ parseExpressionStatement() {
209
+ // 「文字列」を表示。または 値と値を表示。
210
+ const expressions = [];
211
+ expressions.push(this.parseExpression());
212
+
213
+ // 「と」で連結された式
214
+ while (this.check('と')) {
215
+ // 次が「を」でないことを確認(「と」の後に動詞が来る場合は終了)
216
+ const savedPos = this.pos;
217
+ this.advance(); // 「と」を消費
218
+
219
+ // 次の式を読む
220
+ if (this.peek().type === TOKEN_TYPES.STRING ||
221
+ this.peek().type === TOKEN_TYPES.NUMBER ||
222
+ this.checkIdentifier()) {
223
+ expressions.push(this.parseExpression());
224
+ } else {
225
+ // 巻き戻し
226
+ this.pos = savedPos;
227
+ break;
228
+ }
229
+ }
230
+
231
+ // 動詞(を表示、など)
232
+ this.consume('を');
233
+
234
+ if (this.match('表示')) {
235
+ this.consumePeriod();
236
+ return new ASTNode('DisplayStatement', { expressions });
237
+ }
238
+
239
+ throw new Error(`予期しない動詞: ${this.peek().value}`);
240
+ }
241
+
242
+ parseExpression() {
243
+ const token = this.peek();
244
+
245
+ if (token.type === TOKEN_TYPES.STRING) {
246
+ this.advance();
247
+ return new ASTNode('Literal', { value: token.value, raw: token.value });
248
+ } else if (token.type === TOKEN_TYPES.NUMBER) {
249
+ this.advance();
250
+ return new ASTNode('Literal', { value: token.value, raw: token.value });
251
+ } else if (this.match('真')) {
252
+ return new ASTNode('Literal', { value: true, raw: 'true' });
253
+ } else if (this.match('偽')) {
254
+ return new ASTNode('Literal', { value: false, raw: 'false' });
255
+ } else if (this.checkIdentifier()) {
256
+ const name = this.consumeIdentifier();
257
+ return new ASTNode('Identifier', { name });
258
+ }
259
+
260
+ throw new Error(`予期しないトークン: ${token.value}`);
261
+ }
262
+
263
+ // ヘルパーメソッド
264
+ check(keyword) {
265
+ if (this.isAtEnd()) return false;
266
+ return this.peek().type === TOKEN_TYPES.KEYWORD && this.peek().value === keyword;
267
+ }
268
+
269
+ checkIdentifier() {
270
+ if (this.isAtEnd()) return false;
271
+ return this.peek().type === TOKEN_TYPES.IDENTIFIER;
272
+ }
273
+
274
+ checkPeriod() {
275
+ if (this.isAtEnd()) return false;
276
+ return this.peek().type === TOKEN_TYPES.PERIOD;
277
+ }
278
+
279
+ match(...keywords) {
280
+ for (const keyword of keywords) {
281
+ if (this.check(keyword)) {
282
+ this.advance();
283
+ return true;
284
+ }
285
+ }
286
+ return false;
287
+ }
288
+
289
+ consume(keyword) {
290
+ if (this.check(keyword)) {
291
+ return this.advance();
292
+ }
293
+ throw new Error(`期待されるキーワード '${keyword}' が見つかりません。現在: ${this.peek().value}`);
294
+ }
295
+
296
+ consumeComma() {
297
+ if (this.peek().type === TOKEN_TYPES.COMMA) {
298
+ return this.advance();
299
+ }
300
+ throw new Error(`読点(、)が期待されます。現在: ${this.peek().value}`);
301
+ }
302
+
303
+ consumePeriod() {
304
+ if (this.peek().type === TOKEN_TYPES.PERIOD) {
305
+ return this.advance();
306
+ }
307
+ throw new Error(`句点(。)が期待されます。現在: ${this.peek().value}`);
308
+ }
309
+
310
+ consumeIdentifier() {
311
+ if (this.checkIdentifier()) {
312
+ return this.advance().value;
313
+ }
314
+ throw new Error(`識別子が期待されます。現在: ${this.peek().value}`);
315
+ }
316
+
317
+ consumeNumber() {
318
+ if (this.peek().type === TOKEN_TYPES.NUMBER) {
319
+ return this.advance().value;
320
+ }
321
+ throw new Error(`数値が期待されます。現在: ${this.peek().value}`);
322
+ }
323
+
324
+ advance() {
325
+ if (!this.isAtEnd()) this.pos++;
326
+ return this.previous();
327
+ }
328
+
329
+ isAtEnd() {
330
+ return this.peek().type === TOKEN_TYPES.EOF;
331
+ }
332
+
333
+ peek() {
334
+ return this.tokens[this.pos];
335
+ }
336
+
337
+ previous() {
338
+ return this.tokens[this.pos - 1];
339
+ }
340
+ }
341
+
342
+ module.exports = { Parser, ASTNode };
@@ -0,0 +1,126 @@
1
+ // テストスイート
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { compile } = require('../src/compiler');
6
+ const assert = require('assert');
7
+
8
+ function test(name, fn) {
9
+ try {
10
+ fn();
11
+ console.log(`✓ ${name}`);
12
+ } catch (error) {
13
+ console.error(`✗ ${name}`);
14
+ console.error(` ${error.message}`);
15
+ }
16
+ }
17
+
18
+ console.log('KokoScript テスト実行中...\n');
19
+ const examplesDir = path.join(__dirname, 'examples');
20
+
21
+ // 変数宣言のテスト
22
+ test('変数宣言', () => {
23
+ const source = '変数、名前は「太郎」。';
24
+ const result = compile(source);
25
+ assert(result.success);
26
+ assert(result.code.includes('let 名前 = "太郎"'));
27
+ });
28
+
29
+ // 条件分岐のテスト
30
+ test('条件分岐', () => {
31
+ const source = `
32
+ 変数、年齢は25。
33
+ もし、年齢が20より大きいならば、
34
+ 「成人」を表示。
35
+ 終わり。
36
+ `;
37
+ const result = compile(source);
38
+ assert(result.success);
39
+ assert(result.code.includes('if'));
40
+ assert(result.code.includes('年齢 > 20'));
41
+ });
42
+
43
+ // 繰り返しのテスト
44
+ test('繰り返し', () => {
45
+ const source = `
46
+ 3回、繰り返す、
47
+ 「こんにちは」を表示。
48
+ 終わり。
49
+ `;
50
+ const result = compile(source);
51
+ assert(result.success);
52
+ assert(result.code.includes('for'));
53
+ assert(result.code.includes('_i < 3'));
54
+ });
55
+
56
+ // 関数定義のテスト
57
+ test('関数定義', () => {
58
+ const source = `
59
+ 関数、挨拶は、名前で、
60
+ 「こんにちは」と名前を表示。
61
+ 終わり。
62
+ `;
63
+ const result = compile(source);
64
+ assert(result.success);
65
+ assert(result.code.includes('function 挨拶'));
66
+ assert(result.code.includes('名前'));
67
+ });
68
+
69
+ // 関数呼び出しのテスト
70
+ test('関数呼び出し', () => {
71
+ const source = `
72
+ 関数、挨拶は、名前で、
73
+ 「こんにちは」と名前を表示。
74
+ 終わり。
75
+ 挨拶に「太郎」を渡す。
76
+ `;
77
+ const result = compile(source);
78
+ assert(result.success);
79
+ assert(result.code.includes('挨拶("太郎")'));
80
+ });
81
+
82
+ // 表示文のテスト
83
+ test('表示文', () => {
84
+ const source = '「Hello」と「World」を表示。';
85
+ const result = compile(source);
86
+ assert(result.success);
87
+ assert(result.code.includes('console.log'));
88
+ assert(result.code.includes('"Hello" + "World"'));
89
+ });
90
+
91
+ // 数値のテスト
92
+ test('数値リテラル', () => {
93
+ const source = '変数、数は42。';
94
+ const result = compile(source);
95
+ assert(result.success);
96
+ assert(result.code.includes('let 数 = 42'));
97
+ });
98
+
99
+ // 複数文のテスト
100
+ test('複数文', () => {
101
+ const source = `
102
+ 変数、aは1。
103
+ 変数、bは2。
104
+ 「完了」を表示。
105
+ `;
106
+ const result = compile(source);
107
+ assert(result.success);
108
+ assert(result.code.includes('let a = 1'));
109
+ assert(result.code.includes('let b = 2'));
110
+ assert(result.code.includes('console.log'));
111
+ });
112
+
113
+ // サンプルファイルのコンパイル確認
114
+ test('サンプルコード: basic.koko', () => {
115
+ const sample = fs.readFileSync(path.join(examplesDir, 'basic.koko'), 'utf-8');
116
+ const result = compile(sample);
117
+ assert(result.success);
118
+ });
119
+
120
+ test('サンプルコード: advanced.koko', () => {
121
+ const sample = fs.readFileSync(path.join(examplesDir, 'advanced.koko'), 'utf-8');
122
+ const result = compile(sample);
123
+ assert(result.success);
124
+ });
125
+
126
+ console.log('\nテスト完了!');
@@ -0,0 +1,32 @@
1
+ # KokoScript - 応用サンプル
2
+
3
+ # 在庫管理
4
+ 変数、リンゴは3。
5
+ 変数、バナナは5。
6
+
7
+ 「買い物リスト」を表示。
8
+ 「リンゴ: 」とリンゴと「個」を表示。
9
+ 「バナナ: 」とバナナと「本」を表示。
10
+
11
+ # 条件による判断
12
+ もし、リンゴが2より大きいならば、
13
+ 「リンゴが十分あります」を表示。
14
+ そうでなければ、
15
+ 「リンゴを買い足してください」を表示。
16
+ 終わり。
17
+
18
+ # 関数の定義と利用
19
+ 関数、朝挨拶は、名前で、
20
+ 「おはようございます、」と名前と「さん!」を表示。
21
+ 「今日も良い一日を!」を表示。
22
+ 終わり。
23
+
24
+ 朝挨拶に「山田」を渡す。
25
+ 朝挨拶に「佐藤」を渡す。
26
+
27
+ # 繰り返し
28
+ 5回、繰り返す、
29
+ 「準備中...」を表示。
30
+ 終わり。
31
+
32
+ 「準備完了!」を表示。
@@ -0,0 +1,35 @@
1
+ # KokoScript サンプルプログラム
2
+
3
+ # 変数の定義
4
+ 変数、名前は「田中太郎」。
5
+ 変数、年齢は25。
6
+
7
+ # 表示(句読点でキーワードを区切る)
8
+ 「こんにちは、」と名前と「さん」を表示。
9
+
10
+ # 条件分岐(日本の成人年齢は2022年4月から18歳)
11
+ もし、年齢が18より大きいならば、
12
+ 「あなたは成人です」を表示。
13
+ そうでなければ、
14
+ 「あなたは未成年です」を表示。
15
+ 終わり。
16
+
17
+ # 繰り返し
18
+ 3回、繰り返す、
19
+ 「カウント中...」を表示。
20
+ 終わり。
21
+
22
+ # 関数の定義
23
+ 関数、挨拶は、人名で、
24
+ 「ようこそ、」と人名と「さん!」を表示。
25
+ 終わり。
26
+
27
+ # 関数の呼び出し
28
+ 挨拶に「山田花子」を渡す。
29
+ 挨拶に名前を渡す。
30
+
31
+ # 計算を含む例
32
+ 変数、点数は85。
33
+ もし、点数が80より大きいならば、
34
+ 「優秀です!」を表示。
35
+ 終わり。