-
Notifications
You must be signed in to change notification settings - Fork 236
feat(export-to-language): Add DeclarationStore to transpiler #2964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d6ffa08
880146b
f4af2f7
3816816
6238135
c586097
0c59f37
1f58923
7090261
e5202fe
b281d8d
eb132d5
90cac7d
e5a8848
25c1263
0128ecd
0057dcf
410d906
e6e880e
f786206
4ce40ae
acf2867
b2822e0
4a85d5f
fc5f1a6
ff25b11
c3abe60
232c774
6ba5175
10cd687
baaf195
97ae108
d35869f
5752064
60a78fd
da7e26e
5f1a4f5
7296d51
46a167b
3420e5b
f9a7bd4
31ab476
2df04ee
78c97b5
5441bab
97405dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ | |
[![downloads][5]][6] | ||
|
||
Transpilers for building BSON documents in any language. Current support | ||
provided for `shell` `javascript` and `python` as inputs. `java`, `c#`, `node`, `shell` and `python` as | ||
provided for `shell` `javascript` and `python` as inputs. `java`, `c#`, `node`, `shell`, `python` and `ruby` as | ||
outputs. | ||
|
||
> ⚠️ `shell` output produces code that is compatible only with legacy `mongo` shell not the new `mongosh` shell. See [COMPASS-4930](https://jira.mongodb.org/browse/COMPASS-4930) for some additional context | ||
|
@@ -59,6 +59,57 @@ Any transpiler errors that occur will be thrown. To catch them, wrap the | |
- __error.column:__ If it is a syntax error, will have the column. | ||
- __error.symbol:__ If it is a syntax error, will have the symbol associated with the error. | ||
|
||
|
||
### State | ||
|
||
The `CodeGenerationVisitor` class manages a global state which is bound to the `argsTemplate` functions. This state is intended to be used as a solution for the `argsTemplate` functions to communicate with the `DriverTemplate` function. For example: | ||
|
||
```yaml | ||
ObjectIdEqualsArgsTemplate: &ObjectIdEqualsArgsTemplate !!js/function > | ||
(_) => { | ||
this.oneLineStatement = "Hello World"; | ||
return ''; | ||
} | ||
|
||
DriverTemplate: &DriverTemplate !!js/function > | ||
(_spec) => { | ||
return this.oneLineStatement; | ||
} | ||
``` | ||
|
||
The output of the driver syntax for this language will be the one-line statement `Hello World`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool! 🧑🔧 |
||
|
||
#### DeclarationStore | ||
A more practical use-case of state is to accumulate variable declarations throughout the `argsTemplate` to be rendered by the `DriverTemplate`. That is, the motivation for using `DeclarationStore` is to prepend the driver syntax with variable declarations rather than using non-idiomatic solutions such as closures. | ||
|
||
The `DeclarationStore` class maintains an internal state concerning variable declarations. For example, | ||
|
||
```javascript | ||
// within the args template | ||
(arg) => { | ||
return this.declarations.add("Temp", "objectID", (varName) => { | ||
return [ | ||
`${varName}, err := primitive.ObjectIDFromHex(${arg})`, | ||
'if err != nil {', | ||
' log.Fatal(err)', | ||
'}' | ||
].join('\n') | ||
}) | ||
} | ||
``` | ||
|
||
Note that each use of the same variable name will result in an increment being added to the declaration statement. For example, if the variable name `objectIDForTemp` is used two times the resulting declaration statements will use `objectIDForTemp` for the first declaration and `objectID2ForTemp` for the second declaration. The `add` method returns the incremented variable name, and is therefore what would be expected as the right-hand side of the statement defined by the `argsTemplate` function. | ||
|
||
The instance of the `DeclarationStore` constructed by the transpiler class is passed into the driver, syntax via state, for use: | ||
|
||
```javascript | ||
(spec) => { | ||
const comment = '// some comment' | ||
const client = 'client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(cs.String()))' | ||
return "#{comment}\n\n#{client}\n\n${this.declarations.toString()}" | ||
} | ||
``` | ||
|
||
### Errors | ||
There are a few different error classes thrown by `bson-transpilers`, each with | ||
their own error code: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ const { | |
} = require('../helper/error'); | ||
|
||
const { removeQuotes } = require('../helper/format'); | ||
const DeclarationStore = require('./DeclarationStore'); | ||
|
||
/** | ||
* Class for code generation. Goes in between ANTLR generated visitor and | ||
|
@@ -24,6 +25,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi | |
super(); | ||
this.idiomatic = true; // PUBLIC | ||
this.clearImports(); | ||
this.state = { declarations: new DeclarationStore() }; | ||
} | ||
|
||
clearImports() { | ||
|
@@ -53,15 +55,32 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi | |
return this[rule](ctx); | ||
} | ||
|
||
returnResultWithDeclarations(ctx) { | ||
let result = this.returnResult(ctx); | ||
if (this.getState().declarations.length() > 0) { | ||
result = `${this.getState().declarations.toString() + '\n\n'}${result}`; | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
* PUBLIC: This is the entry point for the compiler. Each visitor must define | ||
* an attribute called "startNode". | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Boolean} useDeclarations - prepend the result string with declarations | ||
* @return {String} | ||
*/ | ||
start(ctx) { | ||
return this.returnResult(ctx).trim(); | ||
start(ctx, useDeclarations = false) { | ||
return (useDeclarations ? this.returnResultWithDeclarations(ctx) : this.returnResult(ctx)).trim(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
|
||
getState() { | ||
return this.state; | ||
} | ||
|
||
clearDeclarations() { | ||
this.getState().declarations.clear(); | ||
} | ||
|
||
/** | ||
|
@@ -330,7 +349,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi | |
} | ||
returnFunctionCallLhsRhs(lhs, rhs, lhsType, l) { | ||
if (lhsType.argsTemplate) { | ||
rhs = lhsType.argsTemplate(l, ...rhs); | ||
rhs = lhsType.argsTemplate.bind(this.getState())(l, ...rhs); | ||
} else { | ||
rhs = `(${rhs.join(', ')})`; | ||
} | ||
|
@@ -494,7 +513,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi | |
? lhsType.template() | ||
: defaultT; | ||
const rhs = lhsType.argsTemplate | ||
? lhsType.argsTemplate(lhsArg, ...args) | ||
? lhsType.argsTemplate.bind(this.getState())(lhsArg, ...args) | ||
: defaultA; | ||
const lhs = skipLhs ? '' : lhsArg; | ||
return this.Syntax.new.template | ||
|
@@ -516,7 +535,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi | |
let args = ''; | ||
const keysAndValues = this.getKeyValueList(ctx); | ||
if (ctx.type.argsTemplate) { | ||
args = ctx.type.argsTemplate( | ||
args = ctx.type.argsTemplate.bind(this.getState())( | ||
this.getKeyValueList(ctx).map((k) => { | ||
return [this.getKeyStr(k), this.visit(this.getValue(k))]; | ||
}), | ||
|
@@ -526,7 +545,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi | |
} | ||
ctx.indentDepth--; | ||
if (ctx.type.template) { | ||
return ctx.type.template(args, ctx.indentDepth); | ||
return ctx.type.template.bind(this.getState())(args, ctx.indentDepth); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
|
@@ -545,7 +564,7 @@ module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisi | |
if (ctx.type.argsTemplate) { // NOTE: not currently being used anywhere. | ||
args = visitedElements.map((arg, index) => { | ||
const last = !visitedElements[index + 1]; | ||
return ctx.type.argsTemplate(arg, ctx.indentDepth, last); | ||
return ctx.type.argsTemplate.bind(this.getState())(arg, ctx.indentDepth, last); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It actually replaces the existing |
||
}).join(''); | ||
} else { | ||
args = visitedElements.join(', '); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/** | ||
* Stores declarations for use in the DriverTemplate | ||
* | ||
* @returns {object} | ||
*/ | ||
class DeclarationStore { | ||
constructor() { | ||
this.clear(); | ||
} | ||
|
||
/** | ||
* Add declarations by templateID + varRoot + declaration combo. Duplications will not be collected, rather the add | ||
* method will return the existing declaration's variable name. | ||
* | ||
* @param {string} templateID - Name/alias of the template the declaration is being made for | ||
* @param {string} varRoot - The root of the variable name to be appended by the occurrence count | ||
* @param {function} declaration - The code block to be prepended to the driver syntax | ||
* @returns {string} the variable name with root and appended count | ||
*/ | ||
addVar(templateID, varRoot, declaration) { | ||
// Don't push existing declarations | ||
const current = this.alreadyDeclared(templateID, varRoot, declaration); | ||
if (current !== undefined) { | ||
return current; | ||
} | ||
const varName = this.next(templateID, varRoot); | ||
this.vars[declaration(varName)] = varName; | ||
return varName; | ||
} | ||
|
||
/** | ||
* Add a function to the funcs set | ||
* | ||
* @param {string} fn - String literal of a function | ||
*/ | ||
addFunc(fn) { | ||
if (!this.funcs[fn]) { | ||
this.funcs[fn] = true; | ||
} | ||
} | ||
|
||
alreadyDeclared(templateID, varRoot, declaration) { | ||
const existing = this.candidates(templateID, varRoot); | ||
for (var i = 0; i < existing.length; i++) { | ||
const candidate = `${this.varTemplateRoot(templateID, varRoot)}${i > 0 ? i : ''}`; | ||
const current = this.vars[declaration(candidate)]; | ||
if (current !== undefined) { | ||
return current; | ||
} | ||
} | ||
} | ||
|
||
candidates(templateID, varRoot) { | ||
const varTemplateRoot = this.varTemplateRoot(templateID, varRoot); | ||
return Object.values(this.vars).filter(varName => varName.startsWith(varTemplateRoot)); | ||
} | ||
|
||
clear() { | ||
this.vars = {}; | ||
this.funcs = {}; | ||
} | ||
|
||
length() { | ||
return Object.keys(this.vars).length + Object.keys(this.funcs).length; | ||
} | ||
|
||
next(templateID, varRoot) { | ||
const existing = this.candidates(templateID, varRoot); | ||
|
||
// If the data does not exist in the vars, then the count should append nothing to the variable name | ||
const count = existing.length > 0 ? existing.length : ''; | ||
return `${this.varTemplateRoot(templateID, varRoot)}${count}`; | ||
} | ||
|
||
/** | ||
* Stringify the variable declarations | ||
* | ||
* @param {string} sep - Seperator string placed between elements in the resulting string of declarations | ||
* @returns {string} all the declarations as a string seperated by a line-break | ||
*/ | ||
toString(sep = '\n\n') { | ||
return [...Object.keys(this.vars), ...Object.keys(this.funcs)].join(sep); | ||
} | ||
|
||
varTemplateRoot(templateID, varRoot) { | ||
return `${varRoot}${templateID}`; | ||
} | ||
} | ||
|
||
module.exports = DeclarationStore; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this say
go
?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, we should definitely include
go
. I'll update that on #2991 so that this PR can remain language agnostic. I added ruby for this request.