From 720bf9e26f7b787111acc9a0527142adffea48a0 Mon Sep 17 00:00:00 2001 From: takejohn Date: Thu, 7 Aug 2025 13:18:30 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E6=95=B0=E5=80=A4=E3=83=AA=E3=83=86?= =?UTF-8?q?=E3=83=A9=E3=83=AB=E3=81=AE=E6=95=B4=E6=95=B0=E9=83=A8=E5=88=86?= =?UTF-8?q?=E3=81=BE=E3=81=9F=E3=81=AF=E5=B0=8F=E6=95=B0=E9=83=A8=E5=88=86?= =?UTF-8?q?=E3=81=AE0=E3=82=92=E7=9C=81=E7=95=A5=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/scanner.ts | 45 +++++++++++++------ test/literals.ts | 14 ++++++ .../zero-part-ommitable-decimal-literal.md | 3 ++ 3 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 unreleased/zero-part-ommitable-decimal-literal.md diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index cb86984f..404e889e 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -205,6 +205,10 @@ export class Scanner implements ITokenStream { } case '.': { this.stream.next(); + if (!this.stream.eof && digit.test(this.stream.char as string)) { + const digitToken = this.tryReadDigits(hasLeftSpacing, pos, true); + if (digitToken) return digitToken; + } return TOKEN(TokenKind.Dot, pos, { hasLeftSpacing }); } case '/': { @@ -312,7 +316,7 @@ export class Scanner implements ITokenStream { return TOKEN(TokenKind.CloseBrace, pos, { hasLeftSpacing }); } default: { - const digitToken = this.tryReadDigits(hasLeftSpacing); + const digitToken = this.tryReadDigits(hasLeftSpacing, pos, false); if (digitToken) return digitToken; const wordToken = this.tryReadWord(hasLeftSpacing); @@ -413,26 +417,30 @@ export class Scanner implements ITokenStream { } } - private tryReadDigits(hasLeftSpacing: boolean): Token | undefined { + private tryReadDigits( + hasLeftSpacing: boolean, + pos: { line: number, column: number }, + hasLeadingDot: boolean, + ): Token | undefined { let wholeNumber = ''; let fractional = ''; - const pos = this.stream.getPos(); - - while (!this.stream.eof && digit.test(this.stream.char)) { - wholeNumber += this.stream.char; - this.stream.next(); - } - if (wholeNumber.length === 0) { - return; + if (!hasLeadingDot) { + while (!this.stream.eof && digit.test(this.stream.char)) { + wholeNumber += this.stream.char; + this.stream.next(); + } + if (wholeNumber.length === 0) { + return; + } } - if (!this.stream.eof && this.stream.char === '.') { - this.stream.next(); + const decimalPoint = this.tryReadDecimalPoint(hasLeadingDot); + if (decimalPoint) { while (!this.stream.eof as boolean && digit.test(this.stream.char as string)) { fractional += this.stream.char; this.stream.next(); } - if (fractional.length === 0) { + if (wholeNumber.length === 0 && fractional.length === 0) { throw new AiScriptSyntaxError('digit expected', pos); } } @@ -445,6 +453,17 @@ export class Scanner implements ITokenStream { return TOKEN(TokenKind.NumberLiteral, pos, { hasLeftSpacing, value }); } + private tryReadDecimalPoint(hasLeadingDot: boolean): boolean { + if (hasLeadingDot) { + return true; + } + if (!this.stream.eof && this.stream.char === '.') { + this.stream.next(); + return true; + } + return false; + } + private readStringLiteral(hasLeftSpacing: boolean): Token { let value = ''; const literalMark = this.stream.char; diff --git a/test/literals.ts b/test/literals.ts index 462cb5b7..628d76cb 100644 --- a/test/literals.ts +++ b/test/literals.ts @@ -58,6 +58,20 @@ describe('literal', () => { eq(res, NUM(0.5)); }); + test.concurrent('number (Float with integer zero omitted)', async () => { + const res = await exe(` + <: .5 + `); + eq(res, NUM(0.5)); + }); + + test.concurrent('number (Float with decimal zero omitted)', async () => { + const res = await exe(` + <: 5. + `); + eq(res, NUM(5)); + }); + test.concurrent('arr (separated by comma)', async () => { const res = await exe(` <: [1, 2, 3] diff --git a/unreleased/zero-part-ommitable-decimal-literal.md b/unreleased/zero-part-ommitable-decimal-literal.md new file mode 100644 index 00000000..375d8224 --- /dev/null +++ b/unreleased/zero-part-ommitable-decimal-literal.md @@ -0,0 +1,3 @@ +- 数値リテラルの整数部分または小数部分の0を省略できるようになりました。 + - 整数でない数値リテラルは、整数部分が0の場合、0を省略して記述できるようになりました。 + - 整数の数値リテラルは、小数部分の0を省略して記述できるようになりました。 From 98c14fffeee659a77d6edde28bcaaa2890facdec Mon Sep 17 00:00:00 2001 From: takejohn Date: Sat, 30 Aug 2025 19:36:02 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=87=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E3=82=BF=E3=83=96=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unreleased/zero-part-ommitable-decimal-literal.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unreleased/zero-part-ommitable-decimal-literal.md b/unreleased/zero-part-ommitable-decimal-literal.md index 375d8224..acc87bf0 100644 --- a/unreleased/zero-part-ommitable-decimal-literal.md +++ b/unreleased/zero-part-ommitable-decimal-literal.md @@ -1,3 +1,3 @@ - 数値リテラルの整数部分または小数部分の0を省略できるようになりました。 - - 整数でない数値リテラルは、整数部分が0の場合、0を省略して記述できるようになりました。 - - 整数の数値リテラルは、小数部分の0を省略して記述できるようになりました。 + - 整数でない数値リテラルは、整数部分が0の場合、0を省略して記述できるようになりました。 + - 整数の数値リテラルは、小数部分の0を省略して記述できるようになりました。 From ac8e9033c60bf685802d7feb422d831dfd97852c Mon Sep 17 00:00:00 2001 From: takejohn Date: Wed, 10 Sep 2025 13:57:21 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E3=82=BC=E3=83=AD=E7=9C=81=E7=95=A5?= =?UTF-8?q?=E3=81=A8=E6=8C=87=E6=95=B0=E8=A1=A8=E8=A8=98=E3=81=AE=E7=B5=84?= =?UTF-8?q?=E3=81=BF=E5=90=88=E3=82=8F=E3=81=9B=E3=81=AE=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/literals.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/literals.ts b/test/literals.ts index 1cadea2f..ba2159b1 100644 --- a/test/literals.ts +++ b/test/literals.ts @@ -99,6 +99,48 @@ describe('literal', () => { eq(res, NUM(5)); }); + test.concurrent('number (integer zero omitted, positive exponent without plus sign)', async () => { + const res = await exe(` + <: .5e3 + `); + eq(res, NUM(500)); + }); + + test.concurrent('number (integer zero omitted, positive exponent with plus sign)', async () => { + const res = await exe(` + <: .5e+3 + `); + eq(res, NUM(500)); + }); + + test.concurrent('number (integer zero omitted, negative exponent)', async () => { + const res = await exe(` + <: .5e-3 + `); + eq(res, NUM(0.0005)); + }); + + test.concurrent('number (decimal zero omitted, positive exponent without plus sign)', async () => { + const res = await exe(` + <: 5.e3 + `); + eq(res, NUM(5000)); + }); + + test.concurrent('number (decimal zero omitted, positive exponent with plus sign)', async () => { + const res = await exe(` + <: 5.e+3 + `); + eq(res, NUM(5000)); + }); + + test.concurrent('number (decimal zero omitted, negative exponent)', async () => { + const res = await exe(` + <: 5.e-3 + `); + eq(res, NUM(0.005)); + }); + test.concurrent('arr (separated by comma)', async () => { const res = await exe(` <: [1, 2, 3] From 71d8b2d8691e37ccf358152963a6b646f47e1b53 Mon Sep 17 00:00:00 2001 From: takejohn Date: Fri, 26 Sep 2025 15:12:10 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=E5=B0=8F=E6=95=B0=E7=82=B9=E3=81=AE?= =?UTF-8?q?=E7=9B=B4=E5=BE=8C=E3=81=AB=E3=83=89=E3=83=83=E3=83=88=E3=81=8C?= =?UTF-8?q?=E7=B6=9A=E3=81=8F=E3=81=A8=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AB?= =?UTF-8?q?=E3=81=AA=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parser/scanner.ts | 3 +++ test/syntax.ts | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 835e82a3..c3feae32 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -507,6 +507,9 @@ export class Scanner implements ITokenStream { } const decimalPoint = this.tryReadDecimalPoint(hasLeadingDot); if (decimalPoint) { + if (this.stream.char === '.') { + throw new AiScriptSyntaxError('dot cannot follow a decimal point', this.stream.getPos()); + } while (!this.stream.eof as boolean && digit.test(this.stream.char as string)) { fractional += this.stream.char; this.stream.next(); diff --git a/test/syntax.ts b/test/syntax.ts index 628dd6cf..18f4ed40 100644 --- a/test/syntax.ts +++ b/test/syntax.ts @@ -1,8 +1,8 @@ import * as assert from 'assert'; -import { describe, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { utils } from '../src'; import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value'; -import { AiScriptRuntimeError, AiScriptUnexpectedEOFError } from '../src/error'; +import { AiScriptRuntimeError, AiScriptSyntaxError, AiScriptUnexpectedEOFError } from '../src/error'; import { exe, getMeta, eq } from './testutils'; /* @@ -1485,6 +1485,18 @@ describe('Infix expression', () => { NUM(3) ); }); + + test.concurrent('dot cannot follow a decimal point', async () => { + await expect(() => exe(` + <: 1..to_str() + `)).rejects.toThrow(AiScriptSyntaxError); + }); + + test.concurrent('dot following parenthesis after decimal point', async () => { + eq(await exe(` + <: (1.).to_str() + `), STR('1')); + }); }); describe('if', () => {