Skip to content
Open
48 changes: 35 additions & 13 deletions src/parser/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '/': {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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;
Expand Down
56 changes: 56 additions & 0 deletions test/literals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
16 changes: 14 additions & 2 deletions test/syntax.ts
Original file line number Diff line number Diff line change
@@ -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';

/*
Expand Down Expand Up @@ -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', () => {
Expand Down
3 changes: 3 additions & 0 deletions unreleased/zero-part-ommitable-decimal-literal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- 数値リテラルの整数部分または小数部分の0を省略できるようになりました。
- 整数でない数値リテラルは、整数部分が0の場合、0を省略して記述できるようになりました。
- 整数の数値リテラルは、小数部分の0を省略して記述できるようになりました。
Loading