Skip to content

Commit

Permalink
Add named arguments to python (#128)
Browse files Browse the repository at this point in the history
* All working but re.compile

* Finish named args
  • Loading branch information
aherlihy committed Oct 31, 2018
1 parent 842f397 commit 96cf583
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 107 deletions.
96 changes: 56 additions & 40 deletions packages/bson-transpilers/codegeneration/CodeGenerationVisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi
);
}

/**
* Some grammar definitions are written so that comparisons will chain and add
* nodes with a single child when the expression does *not* match. This is a
* helper method (right now used just by Python) that skips nodes downwards
* until a node with multiple children is found, or a node matches "goal".
*
* @param {ParserRuleContext} ctx
* @param {String} goal - Optional: the name of the child to find.
* @return {ParserRuleContext}
*/
skipFakeNodesDown(ctx, goal) { /* eslint no-unused-vars: 0 */
return ctx;
}

_getType(ctx) {
if (ctx.type !== undefined) {
return ctx;
Expand Down Expand Up @@ -157,21 +171,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi
return null;
}


/**
* Convert between numeric types. Required so that we don't end up with
* strange conversions like 'Int32(Double(2))', and can just generate '2'.
*
* @param {Array} expectedType - types to cast to.
* @param {ParserRuleContext} ctx - ctx to cast from, if valid.
*
* @returns {String} - visited result, or null on error.
*/
castType(expectedType, ctx) {
const result = this.visit(ctx);
const typedCtx = this.findTypedNode(ctx);
const type = typedCtx.type;

compareTypes(expectedType, type, ctx, result) {
// If the types are exactly the same, just return.
if (expectedType.indexOf(type) !== -1 ||
expectedType.indexOf(type.id) !== -1) {
Expand Down Expand Up @@ -206,32 +206,42 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi

// If the expected type is "numeric", accept the number basic & bson types
if (expectedType.indexOf(this.Types._numeric) !== -1 &&
(numericTypes.indexOf(type) !== -1 || (type.code === 106 ||
type.code === 105 || type.code === 104))) {
(numericTypes.indexOf(type) !== -1 || (type.code === 106 ||
type.code === 105 || type.code === 104))) {
return result;
}
// If the expected type is any number, accept float/int/_numeric
if ((numericTypes.some((t) => ( expectedType.indexOf(t) !== -1))) &&
(type.code === 106 || type.code === 105 || type.code === 104 ||
type === this.Types._numeric)) {
type === this.Types._numeric)) {
return result;
}

return null;
}

/**
* Some grammar definitions are written so that comparisons will chain and add
* nodes with a single child when the expression does *not* match. This is a
* helper method (right now used just by Python) that skips nodes downwards
* until a node with multiple children is found, or a node matches "goal".
* Convert between numeric types. Required so that we don't end up with
* strange conversions like 'Int32(Double(2))', and can just generate '2'.
*
* @param {ParserRuleContext} ctx
* @param {String} goal - Optional: the name of the child to find.
* @return {ParserRuleContext}
* @param {Array} expectedType - types to cast to.
* @param {ParserRuleContext} ctx - ctx to cast from, if valid.
*
* @returns {String} - visited result, or null on error.
*/
skipFakeNodesDown(ctx, goal) { /* eslint no-unused-vars: 0 */
return ctx;
castType(expectedType, ctx) {
const result = this.visit(ctx);
const typedCtx = this.findTypedNode(ctx);
let type = typedCtx.type;

let equal = this.compareTypes(expectedType, type, ctx, result);
while (equal === null) {
if (type.type === null) {
return null;
}
type = type.type;
equal = this.compareTypes(expectedType, type, ctx, result);
}
return equal;
}

/**
Expand All @@ -242,10 +252,12 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi
* possible argument types for that index.
* @param {Array} args - empty if no args.
* @param {String} name - The name of the function for error reporting.
* @param {Object} namedArgs - Optional: if named arguments exist, this is the
* mapping of name to default value.
*
* @returns {Array} - Array containing the generated output for each argument.
*/
checkArguments(expected, args, name) {
checkArguments(expected, args, name, namedArgs) {
const argStr = [];
if (args.length === 0) {
if (expected.length === 0 || expected[0].indexOf(null) !== -1) {
Expand All @@ -270,7 +282,9 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi
`Argument count mismatch: too few arguments passed to '${name}'`
);
}
const result = this.castType(expected[i], args[i]);

const toCompare = this.checkNamedArgs(expected[i], args[i], namedArgs);
const result = this.castType(...toCompare);
if (result === null) {
const typeStr = expected[i].map((e) => {
const id = e && e.id ? e.id : e;
Expand Down Expand Up @@ -318,7 +332,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi
// Check arguments
const expectedArgs = lhsType.args;
let rhs = this.checkArguments(
expectedArgs, this.getArguments(ctx), lhsType.id
expectedArgs, this.getArguments(ctx), lhsType.id, lhsType.namedArgs
);

// Apply the arguments template
Expand Down Expand Up @@ -512,7 +526,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi
// Get the original type of the argument
const expectedArgs = lhsType.args;
let args = this.checkArguments(
expectedArgs, this.getArguments(ctx), lhsType.id
expectedArgs, this.getArguments(ctx), lhsType.id, lhsType.namedArgs
);
let argType;

Expand Down Expand Up @@ -601,7 +615,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi
ctx.type = type;

const args = this.checkArguments(
symbolType.args, this.getArguments(ctx), type.id
symbolType.args, this.getArguments(ctx), type.id, symbolType.namedArgs
);

const expectedFlags = this.Syntax.bsonRegexFlags
Expand Down Expand Up @@ -664,15 +678,17 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi
if (argList.length === 2) {
const idiomatic = this.idiomatic;
this.idiomatic = false;
scope = this.visit(this.getArgumentAt(ctx, 1));
this.idiomatic = idiomatic;
scopestr = `, ${scope}`;
if (this.findTypedNode(
this.getArgumentAt(ctx, 1)).type !== this.Types._object) {
const compareTo = this.checkNamedArgs(
[this.Types._object], this.getArgumentAt(ctx, 1), symbolType.namedArgs
);
scope = this.castType(...compareTo);
if (scope === null) {
throw new BsonTranspilersArgumentError(
'Argument type mismatch: Code requires scope to be an object'
'Code expects argument \'scope\' to be object'
);
}
this.idiomatic = idiomatic;
scopestr = `, ${scope}`;
this.requiredImports[113] = true;
this.requiredImports[10] = true;
}
Expand All @@ -697,7 +713,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi
}

const args = this.checkArguments(
lhsType.args, this.getArguments(ctx), lhsType.id
lhsType.args, this.getArguments(ctx), lhsType.id, lhsType.namedArgs
);
const isNumber = this.findTypedNode(
this.getArgumentAt(ctx, 0)).type.code !== 200;
Expand Down
10 changes: 10 additions & 0 deletions packages/bson-transpilers/codegeneration/javascript/Visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,16 @@ module.exports = (CodeGenerationVisitor) => class Visitor extends CodeGeneration
return name ? name.replace('_stmt', '') : 'Expression';
}

/**
* There are no named arguments in javascript, so we can just return directly.
* @param {Array} expected
* @param {ParserRuleContext} node
* @return {Array}
*/
checkNamedArgs(expected, node) {
return [expected, node];
}

/**
* Execute javascript in a sandbox.
*
Expand Down
139 changes: 82 additions & 57 deletions packages/bson-transpilers/codegeneration/python/Visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,63 +314,6 @@ module.exports = (CodeGenerationVisitor) => class Visitor extends CodeGeneration
return this.generateNumericClass(ctx);
}

executeJavascript(input) {
const sandbox = {
RegExp: RegExp
};
const ctx = new Context(sandbox);
const res = ctx.evaluate('__result = ' + input);
ctx.destroy();
return res;
}

findPatternAndFlags(ctx, pythonFlags, targetFlags) {
let pattern;

const symbolType = this.Symbols.re.attr.compile;
const argList = this.getArguments(ctx);
const args = this.checkArguments(symbolType.args, argList, symbolType.id);

// Compile regex without flags
const raw = this.getArgumentAt(ctx, 0).getText();
let str = raw.replace(/^([rubf]?[rubf]["']|'''|"""|'|")/gi, '');
str = str.replace(/(["]{3}|["]|[']{3}|['])$/, '');
try {
const regexobj = this.executeJavascript(`new RegExp(${raw.substr(-1)}${str}${raw.substr(-1)})`);
pattern = regexobj.source;
} catch (error) {
throw new BsonTranspilersRuntimeError(error.message);
}

// Convert flags
if (args.length === 1) {
return [pattern, targetFlags.u];
}

const flagsArg = this.skipFakeNodesDown(argList[1]);
let visited;
if ('expr' in flagsArg.parentCtx) { // combine bitwise flags
visited = flagsArg.xor_expr().map(f => this.visit(f));
} else {
visited = [this.visit(flagsArg)];
}

const translated = visited
.map(f => pythonFlags[f])
.filter(f => f !== undefined);

if (visited.indexOf('256') === -1) { // default is unicode without re.A
translated.push('u');
}

const target = translated
.map(m => targetFlags[m])
.filter(f => f !== undefined);

const flags = target.sort().join('');
return [pattern, flags];
}

processfrom_native(ctx) {
ctx.type = this.Types.BSONRegExp;
const symbolType = this.Symbols.Regex;
Expand Down Expand Up @@ -480,6 +423,63 @@ module.exports = (CodeGenerationVisitor) => class Visitor extends CodeGeneration
*
*/

findPatternAndFlags(ctx, pythonFlags, targetFlags) {
let pattern;

const symbolType = this.Symbols.re.attr.compile;
const argList = this.getArguments(ctx);
const args = this.checkArguments(symbolType.args, argList, symbolType.id, symbolType.namedArgs);

// Compile regex without flags
const raw = this.getArgumentAt(ctx, 0).getText();
let str = raw.replace(/^([rubf]?[rubf]["']|'''|"""|'|")/gi, '');
str = str.replace(/(["]{3}|["]|[']{3}|['])$/, '');
const input = `new RegExp(${raw.substr(-1)}${str}${raw.substr(-1)})`;
try {
const sandbox = {
RegExp: RegExp
};
const context = new Context(sandbox);
const regexobj = context.evaluate('__result = ' + input);
context.destroy();
pattern = regexobj.source;
} catch (error) {
throw new BsonTranspilersRuntimeError(error.message);
}

// Convert flags
if (args.length === 1) {
return [pattern, targetFlags.u];
}

let flagsArg = argList[1];
flagsArg = this.skipFakeNodesDown(this.checkNamedArgs(
[this.Types._integer], flagsArg, symbolType.namedArgs
)[1]);
let visited;
if ('expr' in flagsArg.parentCtx) { // combine bitwise flags
visited = flagsArg.xor_expr().map(f => this.visit(f));
} else {
visited = [this.visit(flagsArg)];
}

const translated = visited
.map(f => pythonFlags[f])
.filter(f => f !== undefined);

if (visited.indexOf('256') === -1) { // default is unicode without re.A
translated.push('u');
}

const target = translated
.map(m => targetFlags[m])
.filter(f => f !== undefined);

const flags = target.sort().join('');
return [pattern, flags];
}


/**
* Want to throw unimplemented for comprehensions instead of reference errors.
* @param {ParserRuleContext} ctx
Expand Down Expand Up @@ -556,6 +556,31 @@ module.exports = (CodeGenerationVisitor) => class Visitor extends CodeGeneration
return name ? name.replace('_stmt', '') : 'Expression';
}

/**
* If a named argument is passed in, then check against the 'namedArgs' array
* instead of positionally.
*
* @param {Array} expected
* @param {ParserRuleContext} node
* @param {Object} namedArgs
* @return {Array}
*/
checkNamedArgs(expected, node, namedArgs) {
const child = this.skipFakeNodesDown(node);
if (namedArgs && 'test' in child && child.test().length > 1) {
const name = child.test()[0].getText();
const value = child.test()[1];
const expectedType = namedArgs[name];
if (expectedType === undefined) {
throw new BsonTranspilersArgumentError(
`Unknown named argument '${name}'`
);
}
return [expectedType.type, value];
}
return [expected, node];
}

/*
*
* Accessor Functions.
Expand Down
6 changes: 6 additions & 0 deletions packages/bson-transpilers/grammars/Python3.g4
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ argument: ( test (comp_for)? |
test '=' test |
'**' test |
'*' test );
//argument
// : test (comp_for)? #RegularArg
// | test '=' test #KeywordArg
// | '**' test #DoubleStarArg
// | '*' test #SingleStarArg
// ;

comp_iter: comp_for | comp_if;
comp_for: (ASYNC)? 'for' exprlist 'in' or_test (comp_iter)?;
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/bson-transpilers/lib/symbol-table/pythontojava.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading

0 comments on commit 96cf583

Please sign in to comment.