Skip to content

Commit

Permalink
feat(pactjs-generator): parse exec code (#1794)
Browse files Browse the repository at this point in the history
* feat(pactjs-generator): parse exec code

* feat(pactjs-generator): export execCodeParser

* fix: types

* doc: update api docs
  • Loading branch information
javadkh2 committed Mar 7, 2024
1 parent c25778d commit be91293
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/pretty-tips-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kadena/pactjs-generator': minor
---

export execCodeParser in order to parse code peroperty of transactions
28 changes: 28 additions & 0 deletions packages/libs/pactjs-generator/etc/pactjs-generator.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
// @alpha (undocumented)
export function contractParser(contract: string, namespace?: string): [IModule[], IPointer];

// @alpha
export function execCodeParser(code: string): undefined | IParsedCode[];

// @alpha (undocumented)
export function generateDts(module: IModule): string;

Expand All @@ -19,6 +22,31 @@ export function generateTemplates(templates: {
template: ITemplate;
}[], version: string): string;

// @alpha (undocumented)
export interface IParsedCode {
// (undocumented)
args: Array<{
string: string;
} | {
int: number;
} | {
decimal: number;
} | {
object: Array<{
property: string;
value: IParsedCode['args'][number];
}>;
} | {
list: Array<IParsedCode['args']>;
}>;
// (undocumented)
function: {
module?: string;
namespace?: string;
name: string;
};
}

// @alpha (undocumented)
export interface ITemplate {
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { unwrapData } from './utils/dataWrapper';
import { functionCallParser } from './utils/functionCallParser';
import { getPointer } from './utils/getPointer';
import { FAILED } from './utils/parser-utilities';

/**
* @alpha
*/
export interface IParsedCode {
function: {
module?: string;
namespace?: string;
name: string;
};
args: Array<
| { string: string }
| { int: number }
| { decimal: number }
| {
object: Array<{ property: string; value: IParsedCode['args'][number] }>;
}
| { list: Array<IParsedCode['args']> }
>;
}
/**
* Parse the regular transaction code;
*
* this does not include deploy contract code; for that use {@link pactParser }
* @example
* const code = '(coin.transfer "alice" "bob" 100)(free.my-contract.my-function "alice" "bob" [100.1 2] \{ "extra" : "some-data" \} )';
* const parsed = execCodeParser(code);
* // const parsed = [
* // \{
* // function: \{ module: 'coin', name: 'transfer' \},
* // args: [\{ string: 'alice' \}, \{ string: 'bob' \}, \{ int: 100 \}],
* // \},
* // \{
* // function: \{
* // namespace: 'free',
* // module: 'my-contract',
* // name: 'my-function',
* // \},
* // args: [
* // \{ string: 'alice' \},
* // \{ string: 'bob' \},
* // \{ list: [\{ decimal: 100.1 \}, \{ int: 2 \}] \},
* // \{ object: [\{ property: 'extra', value: [\{ string: 'some-data' \}] \}] \},
* // ],
* // \},
* // ];
* @alpha
*/
export function execCodeParser(code: string): undefined | IParsedCode[] {
const pointer = getPointer(code);
const result = functionCallParser(pointer);
const data = unwrapData(result);
if (data === FAILED) {
return undefined;
}
return data?.codes as undefined | IParsedCode[];
}
3 changes: 2 additions & 1 deletion packages/libs/pactjs-generator/src/contract/parsing/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import * as moo from 'moo';
* @internal
*/
export const lexer: Lexer = moo.compile({
decimal: /[-+]?(?:[0-9]+[.])?[0-9]+/,
decimal: /[-+]?(?:[0-9]+[.])[0-9]+/,
int: /[-+]?[0-9]+/,
boolean: /true|false/,
model: { match: /@model\s*\[[^\]]*\]/, lineBreaks: true },
// https://pact-language.readthedocs.io/en/stable/pact-reference.html#atoms
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import { execCodeParser } from '../execCodeParser';

describe('execCodeParser', () => {
it('parses pact code', () => {
const code =
'(coin.transfer "alice" "bob" 100)(free.my-contract.my-function "alice" "bob" [100.1 2] { "extra" : "some-data" } )';
const parsed = execCodeParser(code);
expect(parsed).toEqual([
{
function: { module: 'coin', name: 'transfer' },
args: [{ string: 'alice' }, { string: 'bob' }, { int: '100' }],
},
{
function: {
namespace: 'free',
module: 'my-contract',
name: 'my-function',
},
args: [
{ string: 'alice' },
{ string: 'bob' },
{ list: [{ decimal: '100.1' }, { int: '2' }] },
{ object: [{ property: 'extra', value: { string: 'some-data' } }] },
],
},
]);
});

it('returns undefined if code is not parsable - mismatch parenthesis', () => {
// missing closing parenthesis
const code = '(coin.transfer "alice" "bob" 100';
const parsed = execCodeParser(code);
expect(parsed).toEqual(undefined);
});

it('returns undefined if code is not parsable - invalid arg', () => {
// atom as argument
const code = '(coin.transfer alice "bob" 100';
const parsed = execCodeParser(code);
expect(parsed).toEqual(undefined);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,34 @@ describe('lexer', () => {
],
[`(module "start" "end")`, 7, 'string', 'string'],
[`(module 'a-symbol)`, 5, 'symbol'],
[`(module 1)`, 5, 'decimal'],
[`(module 1)`, 5, 'int'],
[`(module 1.0)`, 5, 'decimal'],
[`(module 1.)`, 6, 'decimal'],
[`(module 1.)`, 6, 'int', 'dot'],
[`(module -1.0)`, 5, 'decimal'],
[`'a-symbol`, 1, 'symbol'],
[`100.25`, 1, 'decimal'],
[`-922337203685477580712387461234`, 1, 'decimal'],
[`-922337203685477580712387461234`, 1, 'int'],
[`(and true false)`, 7, 'boolean', 'boolean'],
[
`(module [1, 2, 3])`,
13,
'lsquare',
'decimal',
'int',
'comma',
'decimal',
'int',
'comma',
'decimal',
'int',
'rsquare',
],
[
`{ "foo": (+ 1 2) }`,
`{ "foo": (+ 1 2.0) }`,
14,
'lcurly',
'string',
'colon',
'lparen',
'atom',
'decimal',
'int',
'decimal',
'rparen',
'rcurly',
Expand All @@ -80,15 +80,7 @@ describe('lexer', () => {
'atom',
'rcurly',
],
[
`(bar::quux 1 "hi")`,
9,
'atom',
'dereference',
'atom',
'decimal',
'string',
],
[`(bar::quux 1 "hi")`, 9, 'atom', 'dereference', 'atom', 'int', 'string'],
[`(defun prefix:string (pfx:string str:string) (+ pfx ""))`, 25],
[
`(defun average (a b)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-disable @kadena-dev/no-eslint-disable */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @rushstack/typedef-var */
// In this module, we generate new functions by composing other functions. In order to allow TypeScript to automatically infer the types,
// I had to disable these rules.
import type { IPointer } from './getPointer';
import type { IParser, RuleReturn } from './parser-utilities';
import {
$,
FAILED,
atom,
id,
maybe,
oneOf,
repeat,
restrictedBlock,
seq,
str,
type,
} from './parser-utilities';

export const functionCall = oneOf(
// namespace.module.function
seq(
$('namespace', atom),
id('.'),
$('module', atom),
id('.'),
$('name', atom),
),
// module.function
seq($('module', atom), id('.'), $('name', atom)),
seq($('name', atom)),
);

const object = (rule: IParser) =>
seq(
id('{'),
repeat(
$(
'object',
seq($('property', str), id(':'), $('value', rule), maybe(id(','))),
),
),
id('}'),
);

const list = (rule: IParser) => seq(id('['), repeat($('list', rule)), id(']'));

const code = (rule: IParser) =>
restrictedBlock(
// function
$('function', functionCall),
repeat($('args', rule)),
);

// initiate parser on demand
const lazyParser = <T extends IParser>(parser: () => T): T =>
((pointer: IPointer) => parser()(pointer)) as T;

type ArgParser = IParser<
| RuleReturn<string, 'string' | 'decimal' | 'int'>
| RuleReturn<unknown, 'list'>
| RuleReturn<Record<string, unknown>, 'object'>
| RuleReturn<unknown, 'code'>
>;

const arg: ArgParser = lazyParser(
() =>
oneOf(
// valid args
$('string', str),
$('decimal', type('decimal')),
$('int', type('int')),
// object($('worked', arg)),
// $('list', list(arg)),
object(arg),
list(arg),
// $('object', object(arg)),
$('code', code(arg)),
) as ArgParser,
);

export const functionCallParser = (pointer: IPointer) => {
const rule = repeat($('codes', code(arg)));
const result = rule(pointer);
if (pointer.done()) {
// return the result if all tokens are consumed
return result;
}
return FAILED;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import { getLexerOutput } from '../lexer';
interface IToken {
value: string;
// TODO: complete this list and remove string
type?: 'lparen' | 'rparen' | 'dot' | 'string' | 'namespace' | 'arom' | string;
type?:
| 'lparen'
| 'rparen'
| 'dot'
| 'string'
| 'namespace'
| 'arom'
| 'decimal'
| string;
}

export interface IPointer {
Expand All @@ -21,6 +29,7 @@ export const getPointer = (contract: string): IPointer => {
let idx = -1;
return {
next: () => {
if (idx === tokens.length - 1) return undefined;
idx += 1;
return tokens[idx];
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@ export const block: ISeq = (...parsers) => {
return seqParser(blockPinter);
});
};

export const restrictedBlock: ISeq = (...parsers) => {
const seqParser = seq(...parsers);
return rule((pointer) => {
const token = pointer.next();
if (token?.type !== 'lparen') return FAILED;
const blockPinter = getBlockPointer(pointer);
const result = seqParser(blockPinter);
const lastToken = pointer.next();
if (lastToken?.type !== 'rparen') return FAILED;
return result;
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './rule';
export * from './seq';
export * from './skip';
export * from './str';
export * from './type';
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, expect, it } from 'vitest';
import { unwrapData } from '../../dataWrapper';
import { getPointer } from '../../getPointer';
import { asString } from '../asString';
import { atom } from '../atom';
import { block } from '../block';
import { block, restrictedBlock } from '../block';
import { id } from '../id';
import { $ } from '../inspect';
import { FAILED } from '../rule';
Expand Down Expand Up @@ -43,3 +44,17 @@ describe('block rule', () => {
expect(result.data.title).toBe('developer');
});
});

describe('restrictedBlock rule', () => {
it('should return FAILED if the parentheses are not matched like ((anything) ', () => {
const pointer = getPointer('(( test )');
const result = restrictedBlock(id('test'))(pointer);
expect(result).toBe(FAILED);
});

it('does not skip the rest of the tokens in the block if all rules are already matched', () => {
const pointer = getPointer('( this should fail )');
const result = unwrapData(restrictedBlock($('test', id('this')))(pointer));
expect(result).toBe(FAILED);
});
});
Loading

0 comments on commit be91293

Please sign in to comment.