Browse Source

Added utility methods for objects

Before there used to be some internal utility methods for arrays and objects, but as the code base moved to ES5+ use  case only, these were removed in favour of native alternatives, but most of these were only beneficial for arrays.

This commit add's common utility methods for objects, and also exposes these as they can be used by plugin developer's on the PEG.js AST.
master
Futago-za Ryuu 3 years ago
parent
commit
7cdfc03e9f
  1. 22
      lib/compiler/index.js
  2. 35
      lib/compiler/passes/generate-bytecode.js
  3. 22
      lib/compiler/passes/report-duplicate-labels.js
  4. 3
      lib/compiler/passes/report-duplicate-rules.js
  5. 43
      lib/compiler/visitor.js
  6. 19
      lib/peg.js
  7. 42
      lib/typings/api.d.ts
  8. 34
      lib/typings/modules.d.ts
  9. 30
      lib/util/convert-passes.js
  10. 9
      lib/util/index.js
  11. 114
      lib/util/objects.js
  12. 199
      test/spec/api/pegjs-util.spec.js

22
lib/compiler/index.js

@ -11,26 +11,14 @@ const reportInfiniteRepetition = require( "./passes/report-infinite-repetition"
const reportUndefinedRules = require( "./passes/report-undefined-rules" );
const inferenceMatchResult = require( "./passes/inference-match-result" );
const visitor = require( "./visitor" );
const util = require( "../util" );
function processOptions( options, defaults ) {
const processedOptions = {};
Object.keys( options ).forEach( name => {
processedOptions[ name ] = options[ name ];
} );
Object.keys( defaults ).forEach( name => {
if ( ! Object.prototype.hasOwnProperty.call( processedOptions, name ) ) {
processedOptions[ name ] = defaults[ name ];
}
} );
util.extend( processedOptions, options );
util.extend( processedOptions, defaults );
return processedOptions;
@ -84,9 +72,9 @@ const compiler = {
trace: false
} );
Object.keys( passes ).forEach( stage => {
util.each( passes, stage => {
passes[ stage ].forEach( pass => {
stage.forEach( pass => {
pass( ast, options );

35
lib/compiler/passes/generate-bytecode.js

@ -4,6 +4,7 @@ const asts = require( "../asts" );
const js = require( "../js" );
const op = require( "../opcodes" );
const visitor = require( "../visitor" );
const util = require( "../../util" );
// Generates bytecode.
//
@ -218,20 +219,6 @@ function generateBytecode( ast ) {
}
function cloneEnv( env ) {
const clone = {};
Object.keys( env ).forEach( name => {
clone[ name ] = env[ name ];
} );
return clone;
}
function buildSequence() {
return Array.prototype.concat.apply( [], arguments );
@ -259,7 +246,7 @@ function generateBytecode( ast ) {
function buildCall( functionIndex, delta, env, sp ) {
const params = Object.keys( env ).map( name => sp - env[ name ] );
const params = util.values( env, value => sp - value );
return [ op.CALL, functionIndex, delta, params.length ].concat( params );
}
@ -272,7 +259,7 @@ function generateBytecode( ast ) {
[ op.EXPECT_NS_BEGIN ],
generate( expression, {
sp: context.sp + 1,
env: cloneEnv( context.env ),
env: util.clone( context.env ),
action: null,
reportFailures: context.reportFailures
} ),
@ -370,7 +357,7 @@ function generateBytecode( ast ) {
return buildSequence(
generate( alternatives[ 0 ], {
sp: context.sp,
env: cloneEnv( context.env ),
env: util.clone( context.env ),
action: null,
reportFailures: context.reportFailures
} ),
@ -396,7 +383,7 @@ function generateBytecode( ast ) {
action( node, context ) {
const env = cloneEnv( context.env );
const env = util.clone( context.env );
const emitCall = node.expression.type !== "sequence" || node.expression.elements.length === 0;
const expressionCode = generate( node.expression, {
sp: context.sp + ( emitCall ? 1 : 0 ),
@ -496,7 +483,7 @@ function generateBytecode( ast ) {
labeled( node, context ) {
const env = cloneEnv( context.env );
const env = util.clone( context.env );
context.env[ node.label ] = context.sp + 1;
@ -515,7 +502,7 @@ function generateBytecode( ast ) {
[ op.PUSH_CURR_POS ],
generate( node.expression, {
sp: context.sp + 1,
env: cloneEnv( context.env ),
env: util.clone( context.env ),
action: null,
reportFailures: context.reportFailures
} ),
@ -546,7 +533,7 @@ function generateBytecode( ast ) {
return buildSequence(
generate( node.expression, {
sp: context.sp,
env: cloneEnv( context.env ),
env: util.clone( context.env ),
action: null,
reportFailures: context.reportFailures
} ),
@ -565,7 +552,7 @@ function generateBytecode( ast ) {
const expressionCode = generate( node.expression, {
sp: context.sp + 1,
env: cloneEnv( context.env ),
env: util.clone( context.env ),
action: null,
reportFailures: context.reportFailures
} );
@ -583,7 +570,7 @@ function generateBytecode( ast ) {
const expressionCode = generate( node.expression, {
sp: context.sp + 1,
env: cloneEnv( context.env ),
env: util.clone( context.env ),
action: null,
reportFailures: context.reportFailures
} );
@ -605,7 +592,7 @@ function generateBytecode( ast ) {
return generate( node.expression, {
sp: context.sp,
env: cloneEnv( context.env ),
env: util.clone( context.env ),
action: null,
reportFailures: context.reportFailures
} );

22
lib/compiler/passes/report-duplicate-labels.js

@ -2,29 +2,17 @@
const GrammarError = require( "../../grammar-error" );
const visitor = require( "../visitor" );
const util = require( "../../util" );
const __hasOwnProperty = Object.prototype.hasOwnProperty;
// Checks that each label is defined only once within each scope.
function reportDuplicateLabels( ast ) {
let check;
function cloneEnv( env ) {
const clone = {};
Object.keys( env ).forEach( name => {
clone[ name ] = env[ name ];
} );
return clone;
}
function checkExpressionWithClonedEnv( node, env ) {
check( node.expression, cloneEnv( env ) );
check( node.expression, util.clone( env ) );
}
@ -39,7 +27,7 @@ function reportDuplicateLabels( ast ) {
node.alternatives.forEach( alternative => {
check( alternative, cloneEnv( env ) );
check( alternative, util.clone( env ) );
} );
@ -51,7 +39,7 @@ function reportDuplicateLabels( ast ) {
const label = node.label;
if ( Object.prototype.hasOwnProperty.call( env, label ) ) {
if ( __hasOwnProperty.call( env, label ) ) {
const start = env[ label ].start;

3
lib/compiler/passes/report-duplicate-rules.js

@ -2,6 +2,7 @@
const GrammarError = require( "../../grammar-error" );
const visitor = require( "../visitor" );
const __hasOwnProperty = Object.prototype.hasOwnProperty;
// Checks that each rule is defined only once.
function reportDuplicateRules( ast ) {
@ -13,7 +14,7 @@ function reportDuplicateRules( ast ) {
const name = node.name;
if ( Object.prototype.hasOwnProperty.call( rules, name ) ) {
if ( __hasOwnProperty.call( rules, name ) ) {
const start = rules[ name ].start;

43
lib/compiler/visitor.js

@ -1,5 +1,8 @@
"use strict";
const util = require( "../util" );
const __slice = Array.prototype.slice;
// Simple AST node visitor builder.
const visitor = {
build( functions ) {
@ -10,38 +13,40 @@ const visitor = {
}
function visitNop() {
// Do nothing.
}
const visitNop = util.noop;
function visitExpression( node ) {
const extraArgs = Array.prototype.slice.call( arguments, 1 );
const extraArgs = __slice.call( arguments, 1 );
visit.apply( null, [ node.expression ].concat( extraArgs ) );
}
function visitChildren( property ) {
function visitChildren( children, extraArgs ) {
return function visitProperty( node ) {
const args = [ void 0 ].concat( extraArgs );
const cb = extraArgs.length
? function withArgs( child ) {
const extraArgs = Array.prototype.slice.call( arguments, 1 );
args[ 0 ] = child;
visit.apply( null, args );
node[ property ].forEach( child => {
}
: function withoutArgs( child ) {
visit.apply( null, [ child ].concat( extraArgs ) );
visit( child );
} );
};
};
children.forEach( cb );
}
const DEFAULT_FUNCTIONS = {
grammar( node ) {
const extraArgs = Array.prototype.slice.call( arguments, 1 );
const extraArgs = __slice.call( arguments, 1 );
if ( node.initializer ) {
@ -60,9 +65,9 @@ const visitor = {
initializer: visitNop,
rule: visitExpression,
named: visitExpression,
choice: visitChildren( "alternatives" ),
choice: util.createVisitor( "alternatives", visitChildren ),
action: visitExpression,
sequence: visitChildren( "elements" ),
sequence: util.createVisitor( "elements", visitChildren ),
labeled: visitExpression,
text: visitExpression,
simple_and: visitExpression,
@ -79,15 +84,7 @@ const visitor = {
any: visitNop
};
Object.keys( DEFAULT_FUNCTIONS ).forEach( type => {
if ( ! Object.prototype.hasOwnProperty.call( functions, type ) ) {
functions[ type ] = DEFAULT_FUNCTIONS[ type ];
}
} );
util.extend( functions, DEFAULT_FUNCTIONS );
return visit;

19
lib/peg.js

@ -3,6 +3,7 @@
const GrammarError = require( "./grammar-error" );
const compiler = require( "./compiler" );
const parser = require( "./parser" );
const util = require( "./util" );
const peg = {
// PEG.js version (uses semantic versioning).
@ -11,6 +12,7 @@ const peg = {
GrammarError: GrammarError,
parser: parser,
compiler: compiler,
util: util,
// Generates a parser from a specified grammar and returns it.
//
@ -25,25 +27,10 @@ const peg = {
options = typeof options !== "undefined" ? options : {};
function convertPasses( passes ) {
const converted = {};
Object.keys( passes ).forEach( stage => {
converted[ stage ] = Object.keys( passes[ stage ] )
.map( name => passes[ stage ][ name ] );
} );
return converted;
}
const plugins = "plugins" in options ? options.plugins : [];
const config = {
parser: peg.parser,
passes: convertPasses( peg.compiler.passes )
passes: util.convertPasses( peg.compiler.passes )
};
plugins.forEach( p => {

42
lib/typings/api.d.ts

@ -379,6 +379,48 @@ declare namespace peg {
}
namespace util {
interface IStageMap {
[ stage: string ]
: compiler.ICompilerPass[]
| { [ pass: string ]: compiler.ICompilerPass };
}
function convertPasses( stages: IStageMap ): compiler.IPassesMap;
interface IIterator<R = any> {
( value: any ): R;
( value: any, key: string ): R;
}
function clone( source: {} ): {};
function each( object: {}, iterator: IIterator<void> ): void;
function extend( target: {}, source: {} ): {};
function map( object: {}, transformer: IIterator ): {};
function values( object: {}, transformer?: IIterator ): any[];
interface IVisitor {
( value: {} ): void;
}
interface IVisitorCallback {
( value: any ): void;
( value: any, args: any[] ): void;
}
function createVisitor( property: string | number, visit: IVisitorCallback ): IVisitor;
}
interface IBuildConfig<T = any> {
parser: GeneratedParser<T>;

34
lib/typings/modules.d.ts

@ -133,3 +133,37 @@ declare module "pegjs/lib/compiler/passes/report-undefined-rules" {
export default peg.compiler.passes.check.reportUndefinedRules;
}
declare module "pegjs/lib/util" {
export default peg.util;
}
declare module "pegjs/lib/util/convert-passes" {
export default peg.util.convertPasses;
}
declare module "pegjs/lib/util/index" {
export default peg.util;
}
declare module "pegjs/lib/util/objects" {
namespace objects {
function clone( source: {} ): {};
function each( object: {}, iterator: peg.util.IIterator<void> ): void;
function extend( target: {}, source: {} ): {};
function map( object: {}, transformer: peg.util.IIterator ): {};
function values( object: {}, transformer?: peg.util.IIterator ): any[];
function createVisitor( property: string | number, visit: peg.util.IVisitorCallback ): peg.util.IVisitor;
}
export default objects;
}

30
lib/util/convert-passes.js

@ -0,0 +1,30 @@
"use strict";
// type Pass = ( ast: {}, options: {} ) => void;
// type StageMap = { [string]: { [string]: Pass } };
// type PassMap = { [string]: Pass[] };
//
// The PEG.js compiler runs each `Pass` on the `PassMap` (it's 2nd argument),
// but the compiler api exposes a `StageMap` so that it is easier for plugin
// developer's to access the built-in passes.
//
// This file exposes a method that will take a `StageMap`, and return a
// `PassMap` that can then be passed to the compiler.
const objects = require( "./objects" );
function convertStage( passes ) {
return Array.isArray( passes )
? passes
: objects.values( passes );
}
function convertPasses( stages ) {
return objects.map( stages, convertStage );
}
module.exports = convertPasses;

9
lib/util/index.js

@ -0,0 +1,9 @@
"use strict";
const objects = require( "./objects" );
exports.noop = function noop() { };
exports.convertPasses = require( "./convert-passes" );
objects.extend( exports, objects );

114
lib/util/objects.js

@ -0,0 +1,114 @@
"use strict";
const __hasOwnProperty = Object.prototype.hasOwnProperty;
const __slice = Array.prototype.slice;
const objects = {
// Produce's a shallow clone of the given object.
clone( source ) {
const target = {};
for ( const key in source ) {
if ( ! __hasOwnProperty.call( source, key ) ) continue;
target[ key ] = source[ key ];
}
return target;
},
// Will loop through an object's properties calling the given function.
//
// NOTE:
// This method is just a simplification of:
//
// Object.keys( object ).forEach( key => value = object[ key ] )`
//
// It is not meant to be compatible with `Array#forEach`.
each( object, iterator ) {
for ( const key in object ) {
if ( ! __hasOwnProperty.call( object, key ) ) continue;
iterator( object[ key ], key );
}
},
// This method add's properties from the source object to the target object,
// but only if they don't already exist. It is more similar to how a native
// class is extened then the native `Object.assign`.
extend( target, source ) {
for ( const key in source ) {
if ( ! __hasOwnProperty.call( source, key ) ) continue;
if ( __hasOwnProperty.call( target, key ) ) continue;
target[ key ] = source[ key ];
}
return target;
},
// Is similar to `Array#map`, but, just like `each`, it is not compatible,
// especially because it returns an object rather then an array.
map( object, transformer ) {
const target = {};
for ( const key in object ) {
if ( ! __hasOwnProperty.call( object, key ) ) continue;
target[ key ] = transformer( object[ key ], key );
}
return target;
},
// This return's an array like `Array#map` does, but the transformer method
// is optional, so at the same time behave's like ES2015's `Object.values`.
values( object, transformer ) {
const target = [];
let index = -1;
let key, value;
for ( key in object ) {
if ( ! __hasOwnProperty.call( object, key ) ) continue;
value = object[ key ];
target[ ++index ] = transformer
? transformer( value, key )
: value;
}
return target;
},
// Will return a function that can be used to visit a specific property
// no matter what object is passed to it.
createVisitor( property, visit ) {
return function visitProperty( object ) {
visit( object[ property ], __slice.call( arguments, 1 ) );
};
},
};
module.exports = objects;

199
test/spec/api/pegjs-util.spec.js

@ -0,0 +1,199 @@
"use strict";
const chai = require( "chai" );
const util = require( "pegjs-dev" ).util;
const expect = chai.expect;
describe( "PEG.js Utility API", function () {
describe( "util.convertPasses", function () {
const passes = {
stage1: {
pass1() { },
pass2() { },
pass3() { }
},
stage2: {
pass1() { },
pass2() { }
},
stage3: {
pass1() { }
}
};
function expectPasses( result ) {
expect( result ).to.be.an( "object" );
expect( result.stage1 )
.to.be.an( "array" )
.and.to.have.a.lengthOf( 3 );
expect( result.stage2 )
.to.be.an( "array" )
.and.to.have.a.lengthOf( 2 );
expect( result.stage3 )
.to.be.an( "array" )
.and.to.have.a.lengthOf( 1 );
}
it( "converts a map of stages containing a map of passes", function () {
expectPasses( util.convertPasses( passes ) );
} );
it( "converts a map of stages containing a list of passes", function () {
expectPasses( util.convertPasses( {
stage1: [
passes.stage1.pass1,
passes.stage1.pass2,
passes.stage1.pass3
],
stage2: passes.stage2,
stage3: [
passes.stage3.pass1
]
} ) );
} );
} );
describe( "util.clone", function () {
const meta = { name: "pegjs", version: 0.11, util: util };
it( "shallow clones an object", function () {
expect( util.clone( meta ) )
.to.be.an( "object" )
.that.has.own.includes( meta );
} );
it( "cloned properties refrence same value", function () {
expect( util.clone( meta ) )
.to.haveOwnProperty( "util" )
.that.is.a( "object" )
.which.equals( util );
} );
} );
describe( "util.each", function () {
it( "should iterate over an objects properties", function () {
const size = Object.keys( util ).length;
const entries = [];
util.each( util, ( value, key ) => {
entries.push( { key, value } );
} );
expect( entries.length ).to.equal( size );
entries.forEach( entry => {
expect( util )
.to.have.ownProperty( entry.key )
.which.equals( entry.value );
} );
} );
} );
describe( "util.values", function () {
const map = { a: 1, b: 2, c: 3 };
it( "can extract values like Object.values", function () {
expect( util.values( map ) )
.to.be.an( "array" )
.with.a.lengthOf( 3 )
.and.includes.members( [ 1, 2, 3 ] );
} );
it( "can take a transformer, like Array#map", function () {
expect( util.values( map, n => String( n ) ) )
.that.includes.members( [ "1", "2", "3" ] );
} );
} );
describe( "util.extend", function () {
const source = { d: 4, e: 5, f: 6, g: 7, h: 8 };
it( "extend an empty object", function () {
const target = {};
expect( util.extend( target, source ) )
.to.be.an( "object" )
.that.includes.keys( Object.keys( source ) );
expect( util.values( target ) )
.to.include.members( [ 4, 5, 6, 7, 8 ] )
.and.have.a.lengthOf( 5 );
} );
it( "extend an object", function () {
const target = util.extend( {}, source );
const utils = Object.keys( util );
expect( util.extend( target, util ) )
.to.include.keys( utils );
expect( util.values( target ) )
.to.have.a.lengthOf( 5 + utils.length );
} );
} );
describe( "util.map", function () {
const object = { a: 1, b: 2, c: 3, d: 4 };
const result = util.map( object, String );
it( "returns an object, and not an array, unlike Array#map", function () {
expect( result )
.to.be.an( "object" )
.that.includes.keys( Object.keys( object ) );
} );
it( "applies a transformation on each properties value", function () {
util.each( result, property => {
expect( property ).to.be.a( "string" );
} );
} );
} );
} );
Loading…
Cancel
Save