Skip to content

Commit

Permalink
Improves Primitive Types (#508)
Browse files Browse the repository at this point in the history
* adds bigint as built-in type, changes 'date' to 'Date', adds val converters

* tests added for bigint, Date, and boolean, noted distinct effect from mixed prop
type w/ bools

* omitting BIGINT & DATE from rule auto-converters, not needed at this time
  • Loading branch information
montymxb authored May 27, 2022
1 parent 8257220 commit 74398ff
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 14 deletions.
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

0 comments on commit 74398ff

Please sign in to comment.