Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improves Primitive Types #508

Merged
merged 6 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/langium/src/grammar/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function isCondition(item: unknown): item is Condition {

export type FeatureName = string;

export type PrimitiveType = 'boolean' | 'date' | 'number' | 'string';
export type PrimitiveType = 'Date' | 'bigint' | 'boolean' | 'number' | 'string';

export interface AbstractElement extends AstNode {
readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken;
Expand Down
8 changes: 7 additions & 1 deletion packages/langium/src/grammar/generated/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,13 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar
},
{
"$type": "Keyword",
"value": "date"
"value": "Date",
"elements": []
},
{
"$type": "Keyword",
"value": "bigint",
"elements": []
}
]
},
Expand Down
4 changes: 2 additions & 2 deletions packages/langium/src/grammar/langium-grammar-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ export class LangiumGrammarValidator {

checkTerminalRuleReturnType(rule: ast.TerminalRule, accept: ValidationAcceptor): void {
if (rule.type?.name && !isPrimitiveType(rule.type.name)) {
accept('error', "Terminal rules can only return primitive types like 'string', 'boolean', 'number' or 'date'.", { node: rule.type, property: 'name' });
accept('error', "Terminal rules can only return primitive types like 'string', 'boolean', 'number', 'Date' or 'bigint'.", { node: rule.type, property: 'name' });
}
}

Expand Down Expand Up @@ -627,7 +627,7 @@ export class LangiumGrammarValidator {
}
}

const primitiveTypes = ['string', 'number', 'boolean', 'Date'];
const primitiveTypes = ['string', 'number', 'boolean', 'Date', 'bigint'];

function isPrimitiveType(type: string): boolean {
return primitiveTypes.includes(type);
Expand Down
2 changes: 1 addition & 1 deletion packages/langium/src/grammar/langium-grammar.langium
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ AtomType:
(primitiveType=PrimitiveType | isRef?='@'? refType=[AbstractType]) isArray?='[]'? | keywordType=Keyword;

PrimitiveType returns string:
'string' | 'number' | 'boolean' | 'date';
'string' | 'number' | 'boolean' | 'Date' | 'bigint';

type AbstractType = Interface | Type | Action | ParserRule;

Expand Down
10 changes: 10 additions & 0 deletions packages/langium/src/parser/value-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export class DefaultValueConverter implements ValueConverter {
switch (getRuleType(rule)?.toLowerCase()) {
case 'number': return convertNumber(input);
case 'boolean': return convertBoolean(input);
case 'bigint': return convertBigint(input);
case 'date': return convertDate(input);
default: return input;
}
}
Expand All @@ -70,6 +72,14 @@ export function convertInt(input: string): number {
return parseInt(input);
}

export function convertBigint(input: string): bigint {
return BigInt(input);
}

export function convertDate(input: string): Date {
return new Date(input);
}

export function convertNumber(input: string): number {
return Number(input);
}
Expand Down
145 changes: 136 additions & 9 deletions packages/langium/test/parser/langium-parser-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,112 @@ describe('One name for terminal and non-terminal rules', () => {

});

describe('Boolean value converter', () => {
let parser: LangiumParser;
const content = `
grammar G
entry M: value?='true';
hidden terminal WS: /\\s+/;
`;

beforeAll(async () => {
const grammar = (await helper(content)).parseResult.value;
parser = parserFromGrammar(grammar);
});

function expectValue(input: string, value: unknown): void {
const main = parser.parse(input).value as unknown as { value: unknown };
expect(main.value).toBe(value);
}

test('Should have no definition errors', () => {
expect(parser.definitionErrors).toHaveLength(0);
});

test('Parsed Boolean is correct', () => {
expectValue('true', true);
// normal behavior when a property type can be resolved to only boolean
// gives us true/false values representing the parse result
expectValue('false', false);
expectValue('something-else-entirely', false);
});
});

describe('BigInt Parser value converter', () => {
let parser: LangiumParser;
const content = `
grammar G
entry M: value=BIGINT;
terminal BIGINT returns bigint: /[0-9]+/;
hidden terminal WS: /\\s+/;
`;

beforeAll(async () => {
const grammar = (await helper(content)).parseResult.value;
parser = parserFromGrammar(grammar);
});

function expectValue(input: string, value: unknown): void {
const main = parser.parse(input).value as unknown as { value: unknown };
expect(main.value).toBe(value);
}

test('Should have no definition errors', () => {
expect(parser.definitionErrors).toHaveLength(0);
});

test('Parsed BigInt is correct', () => {
expectValue('149587349587234971', BigInt('149587349587234971'));
expectValue('9007199254740991', BigInt('9007199254740991')); // === 0x1fffffffffffff
});
});

describe('Date Parser value converter', () => {
let parser: LangiumParser;
const content = `
grammar G
entry M: value=DATE;
terminal DATE returns Date: /[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}/;
hidden terminal WS: /\\s+/;
`;

beforeAll(async () => {
const grammar = (await helper(content)).parseResult.value;
parser = parserFromGrammar(grammar);
});

test('Should have no definition errors', () => {
expect(parser.definitionErrors).toHaveLength(0);
});

test('Parsed Date is correct Date object', () => {
const parseResult = parser.parse('2022-10-12T00:00').value as unknown as { value: unknown };
expect(parseResult.value).toEqual(new Date('2022-10-12T00:00'));
});
});

describe('Parser calls value converter', () => {

let parser: LangiumParser;
const content = `
grammar TestGrammar
entry Main: value=(QFN|Number);
entry Main:
'big' BigVal |
'b' value?='true' |
'q' value=QFN |
'n' value=Number |
'd' value=DATE;

QFN returns string: ID ('.' QFN)?;
terminal ID: /\\^?[_a-zA-Z][\\w_]*/;

Number returns number: INT ('.' INT)?;
fragment BigVal: value=BINT 'n';
terminal BINT returns bigint: INT /(?=n)/;

terminal DATE returns Date: /[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2}:[0-9]{2})?/;

Number returns number:
INT ('.' INT)?;
terminal INT returns number: /[0-9]+/;

hidden terminal WS: /\\s+/;
Expand All @@ -230,32 +325,64 @@ describe('Parser calls value converter', () => {
});

function expectValue(input: string, value: unknown): void {
const main = parser.parse(input).value as unknown as { value: string };
const main = parser.parse(input).value as unknown as { value: unknown };
expect(main.value).toBe(value);
}

function expectEqual(input: string, value: unknown): void {
const main = parser.parse(input).value as unknown as { value: unknown };
expect(main.value).toEqual(value);
}

test('Should have no definition errors', () => {
expect(parser.definitionErrors).toHaveLength(0);
});

test('Should parse ID inside of data type rule correctly', () => {
expectValue('^x', 'x');
expectValue('q ^x', 'x');
});

test('Should parse FQN correctly', () => {
expectValue('^x.y.^z', 'x.y.z');
expectValue('q ^x.y.^z', 'x.y.z');
});

test('Should parse FQN with whitespace correctly', () => {
expectValue('^x. y . ^z', 'x.y.z');
expectValue('q ^x. y . ^z', 'x.y.z');
});

test('Should parse FQN with comment correctly', () => {
expectValue('^x./* test */y.^z', 'x.y.z');
expectValue('q ^x./* test */y.^z', 'x.y.z');
});

test('Should parse integer correctly', () => {
expectValue('123', 123);
expectValue('n 123', 123);
});

test('Should parse float correctly', () => {
expectValue('123.5', 123.5);
expectValue('n 123.5', 123.5);
});

test('Should parse bool correctly', () => {
expectValue('b true', true);
// this is the current 'boolean' behavior when a prop type can't be resolved to just a boolean
// either true/undefined, no false in this case
expectValue('b false', undefined);
// ...then no distinguishing between the bad parse case when the type is unclear
expectValue('b asdfg', undefined);
});

test('Should parse BigInt correctly', () => {
expectValue('big 9007199254740991n', BigInt('9007199254740991'));
expectValue('big 9007199254740991', undefined);
expectValue('big 1.1', undefined);
expectValue('big -19458438592374', undefined);
});

test('Should parse Date correctly', () => {
expectEqual('d 2020-01-01', new Date('2020-01-01'));
expectEqual('d 2020-01-01T00:00', new Date('2020-01-01T00:00'));
expectEqual('d 2022-10-04T12:13', new Date('2022-10-04T12:13'));
expectEqual('d 2022-Peach', undefined);
});
});

Expand Down
33 changes: 33 additions & 0 deletions packages/langium/test/validation/grammar-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,39 @@ describe('Checked Named CrossRefs', () => {
});
});

describe('Check grammar with primitives', () => {
const grammar = `
grammar PrimGrammar
entry Expr:
(String | Bool | Num | BigInt | DateObj)*;
String:
'String' val=STR;
Bool:
'Bool' val?='true';
Num:
'Num' val=NUM;
BigInt:
'BigInt' val=BIG 'n';
DateObj:
'Date' val=DATE;
terminal STR: /[_a-zA-Z][\\w_]*/;
terminal BIG returns bigint: /[0-9]+(?=n)/;
terminal NUM returns number: /[0-9]+(\\.[0-9])?/;
terminal DATE returns Date: /[0-9]{4}-{0-9}2-{0-9}2/+;
`.trim();

let validationData: ValidatorData;

// 1. build a parser from this grammar, verify it works
beforeAll(async () => {
validationData = await parseAndValidate(grammar);
});

test('No validation errors in grammar', () => {
expect(validationData.diagnostics.filter(d => d.severity === DiagnosticSeverity.Error)).toHaveLength(0);
});
});

interface ValidatorData {
document: LangiumDocument;
diagnostics: Diagnostic[];
Expand Down