grammar com.septentrio.infrastructure.flax.Flax with org.eclipse.xtext.common.Terminals
generate language "http://www.septentrio.com/infrastructure/flax/Flax"
Flax:
('package' name=QualifiedName)?
(imports+=Import)*
(entities+=Entity)*;
Import:
'import' importedNamespace=QualifiedNameWithWildcard;
QualifiedName:
ID ('.' ID)*;
QualifiedNameWithWildcard:
QualifiedName '.*'?;
Entity:
Tag|Class;
Tag:
'tag' name=ID
( '{' properties+=TagProperty* '}' )?;
TagProperty:
name=ID;
TagReference:
'@' tag=[Tag]
( '(' ( properties+=[TagProperty|QualifiedName] ','? )* ')' )?;
Class:
tagrefs+=TagReference*
'class' name=ID
( 'extends' (parents+=[Class|QualifiedName]','?)+ )?
( '{' sections+=Section* '}' )?;
Section:
PropertySection|MethodSection;
PropertySection:
'properties' '{' properties+=Property* '}';
MethodSection:
'methods' '{' methods+=Method* '}';
Member:
Property|Method;
Property:
tagrefs+=TagReference*
class=[Class|QualifiedName] name=ID;
Method:
tagrefs+=TagReference*
class=[Class|QualifiedName]? name=ID
( '(' (parameters+=Parameter','?)* ')' )?
body=Block;
Block:
'{' (expressions+=Expression)* '}';
Parameter:
class=[Class|QualifiedName] name=ID;
Symbol:
Entity|Parameter|Member;
Expression:
Primary ( {Selection.operand=current} levels+=SelectionLevel )*;
/*
* The duplication of the 'Parameters' fragment together
* with the syntactic predicates is messy. It would be
* better to have one single rule for both and replace
* the cross-reference with a regular ID terminal rule.
*/
Primary returns Expression:
'(' Expression ')' |
{Boolean} value=('true'|'false') |
{Integer} value=INT |
{This} 'this' |
{Metadata} '?' class=[Class|QualifiedName] |
{SymbolReference} symbol=[Symbol] => Parameters?;
SelectionLevel returns Expression:
{MemberSelection} '.' member=[Member] => Parameters?;
fragment Parameters:
parameterized ?= '(' (parameters+=Expression','?)* ')';
The generator would be:
package com.septentrio.infrastructure.flax.generator
import com.septentrio.infrastructure.flax.helper.FlaxHelper
import com.septentrio.infrastructure.flax.language.Class
import com.septentrio.infrastructure.flax.language.MethodSection
import com.septentrio.infrastructure.flax.language.Method
import com.septentrio.infrastructure.flax.language.Block
import com.septentrio.infrastructure.flax.language.Boolean
import com.septentrio.infrastructure.flax.language.Integer
import com.septentrio.infrastructure.flax.language.Metadata
import com.septentrio.infrastructure.flax.language.SymbolReference
import com.septentrio.infrastructure.flax.language.Selection
import com.septentrio.infrastructure.flax.language.MemberSelection
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.naming.IQualifiedNameProvider
import org.eclipse.xtext.generator.AbstractGenerator
import org.eclipse.xtext.generator.IFileSystemAccess2
import org.eclipse.xtext.generator.IGeneratorContext
import com.google.inject.Inject
import org.eclipse.emf.ecore.EObject
import java.util.ArrayDeque
import java.util.Map
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import com.septentrio.infrastructure.flax.language.Member
import com.septentrio.infrastructure.flax.test.Exception
class FlaxMatlabGenerator extends AbstractFlaxGenerator {
private Logger logger = LoggerFactory.getLogger('FlaxGenerator')
@Inject extension FlaxHelper
@Inject extension IQualifiedNameProvider
override def String generate(EObject it, FlaxScope scope) {
logger.debug('''BODY: «it»''')
switch it {
case scope.isAssigned(it):
scope.getVariableFor(it)
Class:
'''
classdef «name» «generateAncestry»
«FOR section : sections»
methods «IF isTestCase»(Test)«ENDIF»
«IF section instanceof MethodSection»
«FOR method : section.methods»
«method.generate(scope)»
«ENDFOR»
«ENDIF»
end
«ENDFOR»
end
'''
Method:
'''
function «name»(this«IF !parameters.empty», «parameters.map[generate(scope)].join(', ')»«ENDIF»)
«body.generateInChildScope»
end
'''
Block:
'''
«getPreambleFor(scope)»
«FOR expression : expressions»
«expression.generate(scope)»;
«ENDFOR»
'''
Selection:
operand.generate(scope) + levels.map[generate(scope)].join
Boolean:
String.valueOf(value)
Integer:
String.valueOf(value)
Metadata:
'''flax.lang.Class.fromName('«class_.fullyQualifiedName»')'''
SymbolReference: {
switch symbol {
Class:
symbol.name
Member:
if (symbol.name.equals('assertInstanceOf') && containingClass.isTestCase)
'''this.flax__assertInstanceOf''' // circumvent sealed method
else
'''this.«symbol.name»'''
default:
throw new Exception('Unhandled symbol type')
} +
'''«IF isParameterized»(«parameters.map[generate(scope)].join(', ')»)«ENDIF»'''
}
MemberSelection:
'''.«member.name»«IF isParameterized»(«parameters.map[generate(scope)].join(', ')»)«ENDIF»''' }
}
private def String getPreambleFor(EObject it, FlaxScope scope) {
logger.debug('''HEADER: «it»''')
switch it {
Block:
'''
«FOR expression: expressions»
«expression.getPreambleFor(scope)»
«ENDFOR»
'''
SymbolReference:
'''
«IF symbol instanceof Class»
«generateVariableAssignment(scope)»
«ENDIF»
«IF parameterized»
«FOR parameter: parameters»
«parameter.getPreambleFor(scope)»
«ENDFOR»
«ENDIF»
'''
Selection:
'''
«operand.getPreambleFor(scope)»
«FOR level: levels»
«level.getPreambleFor(scope)»
«ENDFOR»
'''
MemberSelection:
'''
«IF parameterized»
«FOR parameter: parameters»
«parameter.getPreambleFor(scope)»
«ENDFOR»
«ENDIF»
'''
Metadata:
generateVariableAssignment(scope)
}
}
private def generateAncestry(Class it) {
if (!parents.isEmpty)
'''< «parents.map[fullyQualifiedName].join(' & ')»'''
else if (isTestCase)
'< flax.test.TestCase'
else
'< flax.lang.Object'
}
private def generateVariableAssignment(EObject it, FlaxScope scope) {
val rhs = generate(scope)
val lhs = scope.newVariableFor(it)
'''«lhs» = «rhs»;'''.toString
}
}
abstract class AbstractFlaxGenerator extends AbstractGenerator {
private Logger logger = LoggerFactory.getLogger('AbstractFlaxGenerator')
@Inject extension FlaxHelper
val scopes = new ArrayDeque<FlaxScope>
final override def void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) {
for(e:resource.allContents.toIterable)
switch e {
Class:
fsa.generateFile(e.name+".m",e.generateInChildScope)
}
}
final def generateInChildScope(EObject context) {
val scope = new FlaxScope(this)
scopes.push(scope)
val text = generate(context, scope)
scopes.pop
return text
}
final def isTestCase(Class it) {
val testTag = containingFlax.tags.findFirst[name == 'Test']
logger.debug('''testTag = «testTag»''')
logger.debug(''' tagrefs = «methods.map[tagrefs].flatten»''')
logger.debug(''' «testTag == isSameOrChildOf('flax.test.TestCase')»''')
logger.debug(''' «testTag == methods.map[tagrefs].flatten.exists[tag == testTag]»''')
switch it {
case isSameOrChildOf('flax.test.TestCase'):
true
case methods.map[tagrefs].flatten.exists[tag == testTag]:
true
default:
false
}
}
abstract def String generate(EObject context, FlaxScope scope)
}
class FlaxScope {
private Logger logger = LoggerFactory.getLogger('FlaxScope')
private AbstractFlaxGenerator generator
private extension Map<EObject, String> variables = newHashMap
private int count = 0
new (AbstractFlaxGenerator generator) {
this.generator = generator
}
def newVariableFor(EObject it) {
switch put(it, '''v«count++»''') {
case null: {
logger.debug('''NEW VARIABLE «get(it)» for «it»''')
get(it)
}
default:
throw new Exception('')
}
}
def isAssigned(EObject it) {
containsKey(it)
}
def getVariableFor(EObject it) {
get(it)
}
}
And the stripped generator test class:
package com.septentrio.infrastructure.flax.tests
import com.septentrio.infrastructure.flax.helper.FlaxLibrary
import com.septentrio.infrastructure.flax.test.AssertionFailedException
import matlabcontrol.MatlabProxyFactory
import matlabcontrol.MatlabProxyFactoryOptions
import matlabcontrol.MatlabProxy
import matlabcontrol.MatlabConnectionException
import org.eclipse.xtext.validation.CheckMode
import org.eclipse.xtext.validation.IResourceValidator
import org.eclipse.xtext.generator.GeneratorContext
import org.eclipse.xtext.generator.GeneratorDelegate
import org.eclipse.xtext.generator.JavaIoFileSystemAccess
import org.eclipse.xtext.util.CancelIndicator
import org.eclipse.xtext.junit4.InjectWith
import org.eclipse.xtext.junit4.XtextRunner
import org.eclipse.xtext.junit4.TemporaryFolder
import org.eclipse.xtext.junit4.util.ResourceHelper
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.BeforeClass
import org.junit.AfterClass
import org.junit.Assert
import com.google.inject.Inject
import java.nio.file.Paths
@RunWith(XtextRunner)
@InjectWith(FlaxInjectorProvider)
class FlaxGeneratorTest {
static private TemporaryFolder matlabFolder
static private MatlabProxy proxy
static private String matlabPath
@Inject private GeneratorDelegate generator
@Inject private JavaIoFileSystemAccess access
@Inject private IResourceValidator validator
@Inject extension ResourceHelper resourceHelper
@Inject extension FlaxLibrary
@BeforeClass
def static void setup() throws MatlabConnectionException {
matlabFolder = new TemporaryFolder()
proxy = new MatlabProxyFactory(
new MatlabProxyFactoryOptions.Builder()
.setUsePreviouslyControlledSession(true)
.setMatlabStartingDirectory(matlabFolder.getRoot)
.setProxyTimeout(60000) // 1 minute
.build
).getProxy
matlabPath = 'path'.evalAsString
}
@AfterClass
def static void teardown() {
// proxy.exit()
}
/*
* ----------------------------------------------------
* flax.test package generator test
* ----------------------------------------------------
*/
@Test
def void passingTest() {
'''
class TestCase {
methods {
@Test shouldPass {
assert(true)
}
}
}
'''
.generate
.runTest('TestCase')
}
/*
* ----------------------------------------------------
* Matlab specific code
* ----------------------------------------------------
*/
def private MatlabProxy generate(CharSequence text) {
val resource = text.resource
resource.resourceSet.loadFlaxLibrary
val issues = validator.validate(resource, CheckMode.ALL, CancelIndicator.NullImpl);
if (!issues.isEmpty) {
val message = new StringBuilder()
.append('Validation failed with the following issues:')
.append(new StringBuilder() => [issues.forEach(issue|it.append('\n'+issue))])
.toString
Assert.fail(message)
}
generator.generate(
resource,
access => [outputPath = matlabFolder.getRoot.getAbsolutePath],
new GeneratorContext() => [cancelIndicator = CancelIndicator.NullImpl]
)
'clear all'.eval
'path'.feval(matlabPath)
'cd'.feval(matlabFolder.root.toString)
/*
* ../build/classes/test/ ...
* ../build/resources/matlab/ ...
*/
val jarLocation = class.protectionDomain.codeSource.location
val jarPath = Paths.get(jarLocation.toURI)
val matlabLibraryRootPath = jarPath.resolve('../../resources/main/matlab/')
'addpath'.feval(matlabLibraryRootPath.toString)
return proxy
}
def private runTest(MatlabProxy proxy, String name) {
'''result = runtests('«name»');'''.eval
if ('numel(result)'.evalAsDouble == 0)
throw new Exception('''Test «name» execution ended unexpectdly''')
else if ('result.Failed'.evalAsBoolean == true) {
val report = 'result.Details.DiagnosticRecord.Report'.evalAsString
switch 'result.Details.DiagnosticRecord.Event'.evalAsString {
case 'AssertionFailed':
throw new AssertionFailedException(report)
default:
throw new Exception(report)
}
}
}
static private def eval(CharSequence text) {
text.toString.eval
}
static private def eval(String text) {
proxy.eval(text)
}
static private def evalAsDouble(String expression) {
(expression.returningEval(1).get(0) as double[]).get(0)
}
static private def evalAsBoolean(String expression) {
(expression.returningEval(1).get(0) as boolean[]).get(0)
}
static private def evalAsString(String expression) {
expression.returningEval(1).get(0) as String
}
static private def returningEval(String string, int nargout) {
proxy.returningEval(string, nargout)
}
static private def feval(String functionName, Object ... args) {
proxy.feval(functionName, args)
}
}
The error I see happens in the validation phase:
java.lang.AssertionError: Validation failed with the following issues:
ERROR:Couldn't resolve reference to Symbol 'assert'. (__synthetic0.flax line : 4 column : 13)
at org.junit.Assert.fail(Assert.java:91)
at com.septentrio.infrastructure.flax.tests.FlaxGeneratorTest.generate(FlaxGeneratorTest.java:335)
at com.septentrio.infrastructure.flax.tests.FlaxGeneratorTest.passingTest(FlaxGeneratorTest.java:119)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.eclipse.xtext.junit4.XtextRunner$1.evaluate(XtextRunner.java:49)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
The "assert" symbol is a reference to a method defined in the flax.lang.TestCase class:
package flax.test
import flax.lang.Class
tag Test
class TestCase {
methods {
assert(boolean actual) {}
assertInstanceOf(Class actual) {}
}
}
As you can see this class is not declared as an ancestor of the "TestCase" class definition of the unit test above. If added manually, the error message disappear, which is expected.
Does it make any sense?]]>