From 7cdfc03e9f3e87505a638690c90212a4eeabe631 Mon Sep 17 00:00:00 2001 From: Futago-za Ryuu Date: Sun, 14 Jan 2018 20:44:53 +0000 Subject: [PATCH] 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. --- lib/compiler/index.js | 22 +- lib/compiler/passes/generate-bytecode.js | 35 +-- .../passes/report-duplicate-labels.js | 22 +- lib/compiler/passes/report-duplicate-rules.js | 3 +- lib/compiler/visitor.js | 43 ++-- lib/peg.js | 19 +- lib/typings/api.d.ts | 42 ++++ lib/typings/modules.d.ts | 34 +++ lib/util/convert-passes.js | 30 +++ lib/util/index.js | 9 + lib/util/objects.js | 114 ++++++++++ test/spec/api/pegjs-util.spec.js | 199 ++++++++++++++++++ 12 files changed, 474 insertions(+), 98 deletions(-) create mode 100644 lib/util/convert-passes.js create mode 100644 lib/util/index.js create mode 100644 lib/util/objects.js create mode 100644 test/spec/api/pegjs-util.spec.js diff --git a/lib/compiler/index.js b/lib/compiler/index.js index f7ee24e..fe5f913 100644 --- a/lib/compiler/index.js +++ b/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 ); diff --git a/lib/compiler/passes/generate-bytecode.js b/lib/compiler/passes/generate-bytecode.js index d5dda23..68cb469 100644 --- a/lib/compiler/passes/generate-bytecode.js +++ b/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 } ); diff --git a/lib/compiler/passes/report-duplicate-labels.js b/lib/compiler/passes/report-duplicate-labels.js index ab13144..35746e3 100644 --- a/lib/compiler/passes/report-duplicate-labels.js +++ b/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; diff --git a/lib/compiler/passes/report-duplicate-rules.js b/lib/compiler/passes/report-duplicate-rules.js index 55826a3..b1fb3d0 100644 --- a/lib/compiler/passes/report-duplicate-rules.js +++ b/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; diff --git a/lib/compiler/visitor.js b/lib/compiler/visitor.js index 7fae899..6174267 100644 --- a/lib/compiler/visitor.js +++ b/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; diff --git a/lib/peg.js b/lib/peg.js index 1a09895..7304ef1 100644 --- a/lib/peg.js +++ b/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 => { diff --git a/lib/typings/api.d.ts b/lib/typings/api.d.ts index cbe2e1b..38ac86d 100644 --- a/lib/typings/api.d.ts +++ b/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 { + + ( value: any ): R; + ( value: any, key: string ): R; + + } + + function clone( source: {} ): {}; + function each( object: {}, iterator: IIterator ): 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 { parser: GeneratedParser; diff --git a/lib/typings/modules.d.ts b/lib/typings/modules.d.ts index f819b92..b628246 100644 --- a/lib/typings/modules.d.ts +++ b/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; + 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; + +} diff --git a/lib/util/convert-passes.js b/lib/util/convert-passes.js new file mode 100644 index 0000000..317e41a --- /dev/null +++ b/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; diff --git a/lib/util/index.js b/lib/util/index.js new file mode 100644 index 0000000..526fc7f --- /dev/null +++ b/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 ); diff --git a/lib/util/objects.js b/lib/util/objects.js new file mode 100644 index 0000000..0cf8339 --- /dev/null +++ b/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; diff --git a/test/spec/api/pegjs-util.spec.js b/test/spec/api/pegjs-util.spec.js new file mode 100644 index 0000000..9a18d68 --- /dev/null +++ b/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" ); + + } ); + + } ); + + } ); + +} );