diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts index 30dc4a8d..c3feae32 100644 --- a/src/parser/scanner.ts +++ b/src/parser/scanner.ts @@ -209,6 +209,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 '/': { @@ -321,7 +325,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); @@ -484,26 +488,33 @@ export class Scanner implements ITokenStream { return `u${code}`; } - 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) { + 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(); } - if (fractional.length === 0) { + if (wholeNumber.length === 0 && fractional.length === 0) { throw new AiScriptSyntaxError('digit expected', pos); } } @@ -542,6 +553,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 c5f587e8..07dc7150 100644 --- a/test/literals.ts +++ b/test/literals.ts @@ -85,6 +85,62 @@ describe('literal', () => { `), 'exponent expected'); }); + 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('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] 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', () => { diff --git a/unreleased/zero-part-ommitable-decimal-literal.md b/unreleased/zero-part-ommitable-decimal-literal.md new file mode 100644 index 00000000..acc87bf0 --- /dev/null +++ b/unreleased/zero-part-ommitable-decimal-literal.md @@ -0,0 +1,3 @@ +- 数値リテラルの整数部分または小数部分の0を省略できるようになりました。 + - 整数でない数値リテラルは、整数部分が0の場合、0を省略して記述できるようになりました。 + - 整数の数値リテラルは、小数部分の0を省略して記述できるようになりました。