Skip to content
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 .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 22
cache: 'npm'
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## Unreleased

### Fixed

- Fix tl-b grammar to support case from block.tlb and boc.tlb
- Fix tl-b grammar to support math expressions [issues #121](https://github.com/ton-community/tlb-parser/issues/121)
- Fix tl-b grammar use a Nat field in an expression [issues #120](https://github.com/ton-community/tlb-parser/issues/120)

### Chore

- Upgrade ohm-js to v17.2.1
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"dedent": "^1.7.0",
"jest": "^30.2.0",
"js-yaml": "^4.1.1",
"ts-jest": "^29.4.5",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3"
}
}
15 changes: 13 additions & 2 deletions src/ast/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,6 @@ export class NegateExpr extends Expression {

export class RefExpr extends Expression {}

export type Reference = NameExpr | NumberExpr;

export class NameExpr extends RefExpr {
constructor(readonly name: string) {
super();
Expand All @@ -251,3 +249,16 @@ export class NumberExpr extends RefExpr {
super();
}
}

export class FieldAnonExpr extends RefExpr {
static override readonly _attributes: string[] = ['fields'];

constructor(
readonly fields: FieldDefinition[],
readonly isRef: boolean,
) {
super();
}
}

export type Reference = NameExpr | NumberExpr | FieldAnonExpr | BuiltinExpr;
18 changes: 15 additions & 3 deletions src/grammar/tlb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,21 +139,33 @@ TLB {

BuiltinExpr = BuiltinOneArg | BuiltinZeroArgs
// This needs extra 'Parens' because of '(##)' expr:
BuiltinOneArg = "(" ( builtins_one_arg | Parens<builtins_one_arg> ) RefExpr ")"
BuiltinOneArg = ( builtins_one_arg | Parens<builtins_one_arg> ) SimpleExpr
BuiltinZeroArgs = builtins_zero_args

// It is different from 'Combinator' only in the quantity part:
// we always need at least one argument here and it can be complex.
CombinatorExpr = "(" identifier TypeExpr+ ")"
CombinatorExpr = "(" identifier CombinatorArg+ ")"

SimpleExpr =
| NegateExpr
| MathExpr
| RefExpr
| Parens<SimpleExpr>

SimpleExprNoMath =
| NegateExpr
| RefExpr
| Parens<SimpleExpr>

CombinatorArg =
| CellRefExpr
| BuiltinExpr
| CombinatorExpr
| SimpleExprNoMath
| Parens<TypeExpr>

NegateExpr = "~" SimpleExpr
RefExpr = RefInner | Parens<RefInner>
RefExpr = RefInner | Parens<RefInner> | FieldAnonRef | BuiltinExpr
RefInner = identifier | number


Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Grammar, MatchResult } from 'ohm-js';

import type { Program } from './ast/nodes';
import { buildGrammar, buildAST } from './intermediate';
import { validate } from './validation';

export function parse(input: string, grammar: Grammar | undefined = undefined): MatchResult {
if (grammar === undefined) {
Expand All @@ -12,7 +13,9 @@ export function parse(input: string, grammar: Grammar | undefined = undefined):
}

export function ast(input: string): Program {
return buildAST(input, buildGrammar());
const program = buildAST(input, buildGrammar());
validate(program);
return program;
}

export { NodeVisitor } from './ast/visit';
Expand Down
25 changes: 23 additions & 2 deletions src/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,10 @@ export const exprNodes = {
// Builtins

// eslint-disable-next-line @typescript-eslint/no-explicit-any
BuiltinOneArg(lpar: TerminalNode, expr: Node, arg: Node, _rpar: TerminalNode): any {
BuiltinOneArg(expr: Node, arg: Node): any {
// TODO: validate `expr` to be in allowed set of operators
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return withLocations(new ast.BuiltinOneArgExpr(expr.sourceString as any, arg['expr']()), lpar);
return withLocations(new ast.BuiltinOneArgExpr(expr.sourceString as any, arg['expr']()), expr);
},

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -244,6 +244,27 @@ export const exprNodes = {
// Just drop `()` around an expression, it should be fine
return withLocations(node['expr'](), lpar);
},

// eslint-disable-next-line @typescript-eslint/no-explicit-any
FieldAnonRef(ref: TerminalNode, lpar: TerminalNode, fields: IterationNode, _rpar: TerminalNode): any {
return withLocations(
new ast.FieldAnonExpr(
fields.children.map((field: Node) => field['Field']()),
ref.numChildren !== 0,
),
lpar,
);
},

// eslint-disable-next-line @typescript-eslint/no-explicit-any
CombinatorArg(node: Node): any {
return node['expr']();
},

// eslint-disable-next-line @typescript-eslint/no-explicit-any
SimpleExprNoMath(node: Node): any {
return node['expr']();
},
};

function parseMath(left: Node, ops: IterationNode, rights: IterationNode): ast.Expression {
Expand Down
103 changes: 103 additions & 0 deletions src/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { NodeVisitor } from './ast/visit';
import {
Declaration,
FieldDefinition,
FieldBuiltinDef,
FieldNamedDef,
FieldCurlyExprDef,
Expression,
NameExpr,
Program,
BuiltinOneArgExpr,
BuiltinZeroArgs,
} from './ast/nodes';

/**
* Validates that fields used in curly bracket expressions are integer types
*/
export function validate(program: Program): void {
for (const decl of program.declarations) {
validateDeclaration(decl);
}
}

function validateDeclaration(decl: Declaration): void {
const integerFields = new Set<string>();

for (const field of decl.fields) {
const fieldName = getFieldName(field);
if (fieldName && isNatField(field)) {
integerFields.add(fieldName);
}
}

for (const field of decl.fields) {
if (field instanceof FieldCurlyExprDef) {
validateCurlyExpression(field.expr, integerFields);
}
}
}

function getFieldName(field: FieldDefinition): string | null {
if (field instanceof FieldBuiltinDef) {
return field.name;
}
if (field instanceof FieldNamedDef) {
return field.name;
}
return null;
}

function isNatField(field: FieldDefinition): boolean {
if (field instanceof FieldBuiltinDef) {
return field.type === '#';
}

if (field instanceof FieldNamedDef) {
const expr = field.expr;
if (expr instanceof BuiltinOneArgExpr) {
// ##, #<, #<=
return true;
}
if (expr instanceof BuiltinZeroArgs) {
// #
return true;
}
}

return false;
}

class ValidationVisitor extends NodeVisitor {
private integerFields: Set<string>;
private errors: string[] = [];

constructor(integerFields: Set<string>) {
super();
this.integerFields = integerFields;
}

visitNameExpr(node: NameExpr): void {
const isTypeName = node.name[0] && node.name[0] === node.name[0].toUpperCase();
const isIntegerField = this.integerFields.has(node.name);
if (!isIntegerField && !isTypeName) {
this.errors.push(
`cannot use field '${node.name}' in an expression unless it is either an integer or a type`,
);
}
}

getErrors(): string[] {
return this.errors;
}
}

function validateCurlyExpression(expr: Expression, integerFields: Set<string>): void {
const visitor = new ValidationVisitor(integerFields);
visitor.visit(expr);

const errors = visitor.getErrors();
if (errors.length > 0) {
throw new Error(errors.join('; '));
}
}
16 changes: 14 additions & 2 deletions tests/fixtures/grammar/invalid-one-liners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@

- case: parens on the left of `*`
code: |
temp3 {n:#} = Temp3 (n * (2 + 1));
temp {n:#} = Temp (n * (2 + 1));
errorStart: |
Line 1, col 29: expected
Line 1, col 27: expected

- case: cannot apply operator to type
code: |
_ l:(## 7) { l <= 127 } v:(bits l * 8) = A;
errorStart: |
Line 1, col 35: expected

- case: cannot use a non-Nat field in an expression
code: |
_ v:int7 { v <= 127 } = A;
errorValidate: |
cannot use field 'v' in an expression unless it is either an integer or a type
11 changes: 11 additions & 0 deletions tests/fixtures/grammar/valid-one-liners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,14 @@
_ a:^(uint256) b:^(int256) c:^(## 32) = C;
_ a:^Cell b:^Any c:^(bits256) d:^C = B;
_ a:^# b:^(#< 5) c:^(#<= 10) d:^B = A;

- case: math expressions
code: |
_ l:(## 7) { l <= 127 } v:(bits (l * 8)) = A;

- case: use a Nat field in an expression
code: |
_ v:(## 7) { v <= 127 } = A;
_ n:(#< 32) { n >= 1 } = B n;
_ n:(#<= 32) { n >= 1 } = C n;
_ {n:#} { n >= 1 } = D n;
Loading
Loading