diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/Java.kt b/be-java/src/commonMain/kotlin/lang/temper/be/java/Java.kt index afa3e866..45779e51 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/Java.kt +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/Java.kt @@ -5509,7 +5509,7 @@ object Java { pos: Position, type: QualIdentifier, field: Identifier, - ) : BaseTree(pos), Expression { + ) : BaseTree(pos), Expression, LeftHandSide { override val operatorDefinition get() = JavaOperatorDefinition.Atom override val codeFormattingTemplate: CodeFormattingTemplate diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaBackend.kt b/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaBackend.kt index b9fb0488..15c4058b 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaBackend.kt +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaBackend.kt @@ -136,7 +136,7 @@ class JavaBackend private constructor( private var rootMainClass: QualifiedName? = null override fun translate(finished: TmpL.ModuleSet) = buildList { - JavaTranslator(names, dependenciesBuilder).let { trans -> + JavaTranslator(names, dependenciesBuilder, adjusterFactory).let { trans -> names.scanNames(finished) finished.modules.flatMap { tmpLModule -> trans.translate(tmpLModule) diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaTranslator.kt b/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaTranslator.kt index 0b16946c..eb253a43 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaTranslator.kt +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaTranslator.kt @@ -2,6 +2,8 @@ package lang.temper.be.java import lang.temper.ast.deepSlice import lang.temper.ast.toLispy +import lang.temper.be.BackendAdjuster +import lang.temper.be.BackendAdjusterFactory import lang.temper.be.Dependencies import lang.temper.be.java.JavaOperator.Assign import lang.temper.be.tmpl.FnAutodoc @@ -66,6 +68,7 @@ class JavaTranslator( /** a shared instance for consistent name mapping */ private val topNames: JavaNames, private val dependenciesBuilder: Dependencies.Builder? = null, + private val adjusterFactory: BackendAdjusterFactory? = null, ) { /** Spin off an instance for a given module */ private fun forModule(module: ModuleName, programMeta: J.ProgramMeta = J.ProgramMeta(unknownPos)): ModuleScope = @@ -132,6 +135,7 @@ class JavaTranslator( /** Might even stay null for snippets. */ private var module: TmpL.Module? = null + private var adjuster: BackendAdjuster? = null private fun activeDecls(decls: MutableList) = when { processingTestCode -> moduleTestDecls @@ -157,6 +161,7 @@ class JavaTranslator( fun module(module: TmpL.Module): List { this.module = module + adjuster = adjusterFactory?.makeAdjuster(module, this) val result = module.result topLevels@ for (tl in module.topLevels) { try { @@ -236,6 +241,7 @@ class JavaTranslator( addAll(moduleTestDecls) } } + adjuster?.adjustFilesAfterTranslation(programs) return programs } @@ -481,7 +487,9 @@ class JavaTranslator( }.also { add(it.asNameExpr().asArgument()) } } }, - ).exprOrReturnStatement(shouldReturn = result !is J.VoidType).also { add(it) } + ).let { call -> + adjuster?.adjustConnectedCall(fn, call as J.ExpressionStatementExpr) ?: call + }.exprOrReturnStatement(shouldReturn = result !is J.VoidType).also { add(it) } }.let { J.BlockStatement(pos, it) } } @@ -517,7 +525,7 @@ class JavaTranslator( private fun moduleFunction(t: TmpL.FunctionDeclaration) { val autodoc = autodocFor(t) val name = names.moduleFunction(t.name).second.toIdentifier(t.name.pos) - val result = resultType(names, t.returnType, pos = t.returnType.pos) + val result = resultType(t) val body = when { t.metadata.any { it.key.symbol == connectedSymbol } && module?.isStdLib != true -> connectedBody(t, result) @@ -898,7 +906,7 @@ class JavaTranslator( } } is TmpL.Getter -> { - val result = resultType(names, m.returnType, m.pos) + val result = resultType(m) val name = names.getterName(m.dotName, JavaType.toFrontend(m.returnType.ot)) val parameters = parameters(m.parameters) val prop = t.members.firstNotNullOfOrNull { @@ -924,7 +932,7 @@ class JavaTranslator( ) } is TmpL.Setter -> { - val result = resultType(names, m.returnType, m.pos) + val result = resultType(m) val name = names.setterName(m.dotName) val parameters = parameters(m.parameters) add( @@ -958,7 +966,7 @@ class JavaTranslator( J.ModStatic.Dynamic }, ) - val tentativeResult = resultType(names, m.returnType, m.pos) + val tentativeResult = resultType(m) val boxedTypeAdjustments = (m as? TmpL.NormalMethod)?.let { findJavaParametersThatNeedAdjustmentToBoxedType( @@ -1066,7 +1074,7 @@ class JavaTranslator( modAccess = access(m), modFinal = final(m.assignOnce), ), - type = JavaType.fromTmpL(m.type, names).toTypeAst(m.pos), + type = varType(m), variable = names.field(m.name), initializer = null, ), @@ -1082,7 +1090,7 @@ class JavaTranslator( modFinal = final(m.assignOnce), modStatic = J.ModStatic.Static, ), - type = JavaType.fromTmpL(m.type, names).toTypeAst(m.pos), + type = varType(m), variable = names.staticField(m.dotName), initializer = expr(m.expression), ), @@ -1164,7 +1172,7 @@ class JavaTranslator( pos = m.pos, autodoc = autodocFor(m), body = m.body, - result = resultType(names, m.returnType, m.pos), + result = resultType(m), name = names.getterName(m.dotName, JavaType.toFrontend(m.returnType.ot)), params = parameters(m.parameters), ), @@ -1174,7 +1182,7 @@ class JavaTranslator( pos = m.pos, autodoc = autodocFor(m), body = m.body, - result = resultType(names, m.returnType, m.pos), + result = resultType(m), name = names.setterName(m.dotName), params = parameters(m.parameters), ), @@ -1184,7 +1192,7 @@ class JavaTranslator( pos = m.pos, autodoc = autodocFor(m), body = m.body, - result = resultType(names, m.returnType, m.pos), + result = resultType(m), name = names.method(m.dotName).toIdentifier(m.dotName.pos), params = parameters(m.parameters), typeParams = typeFormals(m.typeParameters), @@ -1196,7 +1204,7 @@ class JavaTranslator( autodoc = autodocFor(m), body = m.body, name = names.method(m.dotName).toIdentifier(m.dotName.pos), - result = resultType(names, m.returnType, m.pos), + result = resultType(m), params = parameters(m.parameters), typeParams = typeFormals(m.typeParameters), isStatic = true, @@ -1211,7 +1219,7 @@ class JavaTranslator( J.InterfaceFieldDeclaration( pos = m.pos, javadoc = javadoc(autodocFor(m.pos, m.metadata)), - type = JavaType.fromTmpL(m.type, names).toTypeAst(m.type.pos), + type = varType(m), variables = listOf( J.VariableDeclarator( m.pos, @@ -1243,6 +1251,28 @@ class JavaTranslator( ) } + fun resultType(fn: TmpL.FunctionDeclarationOrMethod): J.ResultType { + // TODO Some manual calls chose the return type pos but most chose the function pos. + // TODO Maybe makes sense to standardize here, but which is best? + return resultType(names, fn.returnType, fn.pos) + } + + fun resultType(type: TmpL.AType, pos: Position? = null): J.ResultType { + return resultType(names, type, pos ?: type.pos) + } + + fun varType(property: TmpL.Property): J.Type { + return varType(property.type, property.pos) + } + + fun varType(v: TmpL.VarLike): J.Type { + return JavaType.fromFrontend(v.descriptor, names).toTypeAst(v.pos) + } + + fun varType(type: TmpL.AType, pos: Position? = null): J.Type { + return JavaType.fromTmpL(type, names).toTypeAst(pos ?: type.pos) + } + /** Create an object containing the method parameters and necessary preamble statements. */ private fun parameters(px: TmpL.Parameters): ParamsPreamble { val rest = px.restParameter @@ -1405,8 +1435,7 @@ class JavaTranslator( scope.addDecl( J.FieldDeclaration( pos, - type = JavaType.fromTmpL(param.type, names) - .toTypeAst(pos), + type = varType(param), variable = localName.outName.toIdentifier(varId.pos), initializer = newNames[oldName.outName]!!.toNameExpr(varId.pos), ), @@ -1452,7 +1481,7 @@ class JavaTranslator( scope.addDecl( J.FieldDeclaration( stmt.pos, - type = JavaType.fromTmpL(stmt.type, names).toTypeAst(stmt.pos), + type = varType(stmt), variable = localName.outName.toIdentifier(varId.pos), initializer = stmt.init?.let(::expr), ), @@ -1493,7 +1522,7 @@ class JavaTranslator( stmts.add( J.LocalVariableDeclaration( pos, - type = JavaType.fromTmpL(param.type, names).toTypeAst(pos), + type = varType(param), name = newName.toIdentifier(varId.pos), expr = oldName.toIdentifier(varId.pos).asNameExpr(), ), @@ -1569,7 +1598,7 @@ class JavaTranslator( val javaType: J.Type get() = JavaType.fromSig(funcType, names).toTypeAst(pos) /** the result type of the function */ - val javaResultType: J.ResultType get() = resultType(names, tmplFunc.returnType, tmplFunc.returnType.pos) + val javaResultType: J.ResultType get() = resultType(tmplFunc) /** a lambda expression can be used in a local variable declaration, or also in a forward declared form. */ fun toLambdaExpr() = @@ -1659,7 +1688,7 @@ class JavaTranslator( private fun localVar(t: TmpL.LocalDeclaration) = J.LocalVariableDeclaration( t.pos, - type = JavaType.fromTmpL(t.type, names).toTypeAst(t.pos), + type = varType(t), name = names.lookupRegularLocalNameObj(t.name).outName.toIdentifier(t.name.pos), expr = t.init?.let(::expr), ) diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/NameHelpers.kt b/be-java/src/commonMain/kotlin/lang/temper/be/java/NameHelpers.kt index ac3edbbe..630d4e4d 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/NameHelpers.kt +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/NameHelpers.kt @@ -46,4 +46,4 @@ internal fun ResolvedName.simpleText(): String = when (this) { internal fun ResolvedName.distinctSafeText() = distinctText().safeIdentifier() /** Apply a simple set of rules to extract a name's text and ensure the identifier is safe for Java. */ -internal fun ResolvedName.simpleSafeText() = simpleText().safeIdentifier() +fun ResolvedName.simpleSafeText() = simpleText().safeIdentifier() diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/SimplifyNames.kt b/be-java/src/commonMain/kotlin/lang/temper/be/java/SimplifyNames.kt index 5dea0c31..cbc85213 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/SimplifyNames.kt +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/SimplifyNames.kt @@ -446,6 +446,7 @@ class SimplifyNames(private val top: J.TopLevelClassDeclaration) { private fun Scope.scanLhs(e: J.LeftHandSide) = when (e) { is J.FieldAccessExpr -> scanExpr(e.expr) is J.NameExpr -> scanName(e) + is J.StaticFieldAccessExpr -> importType(e.type) } private fun Scope.scanArgs(ax: Iterable) = ax.forEach { scanExpr(it.expr) } diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/java.out-grammar b/be-java/src/commonMain/kotlin/lang/temper/be/java/java.out-grammar index 7f6ba780..1b6b08ab 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/java.out-grammar +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/java.out-grammar @@ -1266,7 +1266,7 @@ AssignmentExpr requires `operator.operator.isAssignment()` ; * The target of an assignment may be a name, or an accessor. * JLS 15.26 TODO add array access */ -LeftHandSide = NameExpr | FieldAccessExpr ; +LeftHandSide = NameExpr | FieldAccessExpr | StaticFieldAccessExpr ; /** * A lambda expression. JLS 15.27 diff --git a/be-test-helpers/src/commonMain/kotlin/lang/temper/be/FunctionalTestRunner.kt b/be-test-helpers/src/commonMain/kotlin/lang/temper/be/FunctionalTestRunner.kt index dee5ace3..bb14a18a 100644 --- a/be-test-helpers/src/commonMain/kotlin/lang/temper/be/FunctionalTestRunner.kt +++ b/be-test-helpers/src/commonMain/kotlin/lang/temper/be/FunctionalTestRunner.kt @@ -131,7 +131,7 @@ abstract class FunctionalTestRunner>( val backendOrganization = organizeBackends( listOf(backendId), lookupFactory = ::lookupFactory, - onMissingFactory = { error(it) }, + onError = { error(it) }, ) // TODO Actually build by buckets? val outputRoot = OutputRoot(MemoryFileSystem()) @@ -160,6 +160,7 @@ abstract class FunctionalTestRunner>( outputRoot = outputRoot, preparedModules = preparedModules, test = test, + adjusterFactory = backendOrganization.adjusterFactories[neededBackendId], ) } @@ -204,6 +205,7 @@ abstract class FunctionalTestRunner>( outputRoot: OutputRoot, preparedModules: PreparedFunctionalTest, test: FunctionalTestBase, + adjusterFactory: BackendAdjusterFactory?, ) = run { val supportedBackendList = listOf(backendId) val functionalTestLibraryConfiguration = LibraryConfiguration( @@ -313,6 +315,7 @@ abstract class FunctionalTestRunner>( config = config, dependenciesBuilder = dependenciesBuilder, rawBackendFiles = rawBackendFiles, + adjusterFactory = adjusterFactory, ), ) } diff --git a/be-test-helpers/src/commonMain/kotlin/lang/temper/be/GeneratedCodeHelper.kt b/be-test-helpers/src/commonMain/kotlin/lang/temper/be/GeneratedCodeHelper.kt index 9cba9755..764caf85 100644 --- a/be-test-helpers/src/commonMain/kotlin/lang/temper/be/GeneratedCodeHelper.kt +++ b/be-test-helpers/src/commonMain/kotlin/lang/temper/be/GeneratedCodeHelper.kt @@ -90,7 +90,7 @@ fun > generateCode( val backendOrganization = organizeBackends( listOf(factory.backendId), lookupFactory = lookupFactory, - onMissingFactory = { error(it) }, + onError = { error(it) }, ) for (bucket in backendOrganization.backendBuckets) { for (backendId in bucket) { @@ -103,6 +103,7 @@ fun > generateCode( moduleResultNeeded = moduleResultNeeded, logSink = logSink, outputRoot = outputRoot, + adjusterFactory = factory.adjusterFactories()[backendId], ) } } @@ -117,6 +118,7 @@ fun > generateCode( moduleResultNeeded: Boolean, logSink: LogSink, outputRoot: OutputRoot, + adjusterFactory: BackendAdjusterFactory?, activeFactories: Iterable> = listOf(factory), ) { val backendId = factory.backendId @@ -197,6 +199,7 @@ fun > generateCode( logSink, NullDependencyResolver, backendConfig, + adjusterFactory = adjusterFactory, ), ) } diff --git a/be/src/commonMain/kotlin/lang/temper/be/Backend.kt b/be/src/commonMain/kotlin/lang/temper/be/Backend.kt index 936e6ce1..752e1168 100644 --- a/be/src/commonMain/kotlin/lang/temper/be/Backend.kt +++ b/be/src/commonMain/kotlin/lang/temper/be/Backend.kt @@ -109,6 +109,7 @@ abstract class Backend>( */ val dependenciesBuilder: Dependencies.Builder, val rawBackendFiles: Map = mapOf(), + val adjusterFactory: BackendAdjusterFactory? = null, ) { constructor(backendId: BackendId, setup: BackendSetup) : this( backendId = backendId, @@ -121,6 +122,7 @@ abstract class Backend>( config = setup.config, dependenciesBuilder = setup.dependenciesBuilder, rawBackendFiles = setup.rawBackendFiles, + adjusterFactory = setup.adjusterFactory, ) val libraryConfigurations = dependenciesBuilder.libraryConfigurations @@ -830,6 +832,12 @@ abstract class Backend>( * [module list][BackendSetup.modules]. */ fun make(setup: BackendSetup): Backend + + /** + * Adjuster by backend as wanted. Should only be given for backend ids + * in [BackendMeta.requiredBackendIds] for this backend. + */ + fun adjusterFactories(): Map = mapOf() } @Retention(AnnotationRetention.RUNTIME) @@ -898,8 +906,62 @@ data class BackendSetup>( val config: Backend.Config, /** Files matching backend extensions from the library source tree. */ val rawBackendFiles: Map = mapOf(), + val adjusterFactory: BackendAdjusterFactory? = null, ) +/** Enables module-based lifetimes on individual [BackendAdjuster] instances. */ +interface BackendAdjusterFactory { + fun makeAdjuster(module: TmpL.Module, translator: Any): BackendAdjuster +} + +/** + * Adjusts, customized, or transforms backend behavior. Might be installed by + * one backend on another in some custom Temper configuration. + */ +interface BackendAdjuster { + /** + * Optionally customizes an already translated connected call. On null + * return value, use the original. + * + * Adjusters should return null for any [T] they don't know how to handle + * although typically this is well-defined for a particular backend. + */ + fun > adjustConnectedCall(decl: TmpL.FunctionDeclaration, call: T): T? { + return null + } + + /** + * Pass any already built files. Most common behavior would be to add more. + * The adjust may want to reference existing files and could technically + * modify them. + */ + fun > adjustFilesAfterTranslation(files: MutableList) { + // Do nothing by default. + } +} + +/** Combines two adjuster factories with [this] as priority. */ +fun BackendAdjusterFactory.orElse(other: BackendAdjusterFactory): BackendAdjusterFactory { + return object : BackendAdjusterFactory { + override fun makeAdjuster(module: TmpL.Module, translator: Any): BackendAdjuster { + val a = this@orElse.makeAdjuster(module, translator) + val b = other.makeAdjuster(module, translator) + return object : BackendAdjuster { + /** Only lets the secondary adjuster apply if the first returns null. */ + override fun > adjustConnectedCall(decl: TmpL.FunctionDeclaration, call: T): T? { + return a.adjustConnectedCall(decl, call) ?: b.adjustConnectedCall(decl, call) + } + + /** Adjusts in reverse order, so the priority adjuster has the final say. */ + override fun > adjustFilesAfterTranslation(files: MutableList) { + b.adjustFilesAfterTranslation(files) + a.adjustFilesAfterTranslation(files) + } + } + } + } +} + private fun sourceMapFile(outputSourceFile: FilePath): FilePath { check(outputSourceFile.isFile) val dir = outputSourceFile.dirName() @@ -937,13 +999,38 @@ data class BackendOrganization( /** The factory for each backend. */ val factoriesById: Map>, + + /** Priority order rather than chain. */ + val adjusterFactories: Map = mapOf(), ) +data class BackendOrganizationError( + val kind: BackendOrganizationErrorKind, + val backendId: BackendId, + val sourceBackendId: BackendId? = null, +) + +enum class BackendOrganizationErrorKind { + /** + * A factory was missing for some requested [BackendId], whether in the + * initial list or as some listed, transitive requirement of one. + */ + FactoryNotFound, + + /** + * Required backends currently are in [BackendMeta.requiredBackendIds], + * which really only can express basic metadata, not things like adjusters. + * So adjusters are defined on the factory, but they are expected to be + * specified only for directly required backends. + */ + AdjusterForUnrequiredBackend, +} + /** Calculate transitive backend organization as needed by the given initially requested [backendIds]. */ fun organizeBackends( backendIds: Iterable, lookupFactory: (BackendId) -> Backend.Factory<*>?, - onMissingFactory: (BackendId) -> Unit, + onError: (BackendOrganizationError) -> Unit, ): BackendOrganization { // We need to order how we drive backends. // A backend for one target language might require translations for another target language. @@ -959,7 +1046,10 @@ fun organizeBackends( fun factoryFor(backendId: BackendId) = factories.getOrPut(backendId) { lookupFactory(backendId).also { factory -> if (factory == null) { - onMissingFactory(backendId) + BackendOrganizationError( + kind = BackendOrganizationErrorKind.FactoryNotFound, + backendId = backendId, + ).also { onError(it) } } else { backendOrdering[backendId] = factory.backendMeta.requiredBackendIds.toSet() } @@ -1009,9 +1099,28 @@ fun organizeBackends( } } val backendRequirements = transitiveClosure(backendOrderingFounds).mapValues { setOf(it.key) + it.value } + val adjusterFactories = buildMap { + for (factory in factoriesById.values) { + val adjusters = factory.adjusterFactories() + val requiredBackendIds = factory.backendMeta.requiredBackendIds + for ((backendId, adjuster) in adjusters) { + if (backendId !in requiredBackendIds) { + BackendOrganizationError( + kind = BackendOrganizationErrorKind.AdjusterForUnrequiredBackend, + backendId = backendId, + sourceBackendId = factory.backendId, + ).also { onError(it) } + } + compute(backendId) { _, previous -> + previous?.let { previous.orElse(adjuster) } ?: adjuster + } + } + } + } return BackendOrganization( backendBuckets = backendBuckets, backendRequirements = backendRequirements, factoriesById = factoriesById, + adjusterFactories = adjusterFactories, ) } diff --git a/be/src/commonTest/kotlin/lang/temper/be/BackendTest.kt b/be/src/commonTest/kotlin/lang/temper/be/BackendTest.kt index 017e2893..0ce934c1 100644 --- a/be/src/commonTest/kotlin/lang/temper/be/BackendTest.kt +++ b/be/src/commonTest/kotlin/lang/temper/be/BackendTest.kt @@ -1,39 +1,108 @@ package lang.temper.be +import lang.temper.ast.OutTree import lang.temper.be.tmpl.TestBackend +import lang.temper.be.tmpl.TmpL import lang.temper.lexer.defaultClassifyTemperSource import lang.temper.library.LibraryConfiguration import lang.temper.log.dirPath import lang.temper.log.filePath import lang.temper.name.BackendId +import lang.temper.name.BuiltinName import lang.temper.name.DashedIdentifier import lang.temper.name.ModuleName import kotlin.test.Test +import kotlin.test.assertContentEquals import kotlin.test.assertEquals class BackendTest { - private val helloWorldLibraryConfig = LibraryConfiguration( - libraryName = DashedIdentifier.from("hello-world")!!, - libraryRoot = dirPath("a"), - supportedBackendList = emptyList(), - classifyTemperSource = ::defaultClassifyTemperSource, - ) + @Test + fun backendAdjusters() { + val tmpl = TmplGenerator(".test") + + // Define some adjusters and factories. + class TestAdjusterA : BackendAdjuster { + override fun > adjustConnectedCall(decl: TmpL.FunctionDeclaration, call: T): T? { + // Abusively just treat ids as calls to simplify. + call is TmpL.Id || return null + return when (call.name.displayName) { + "_" -> { + @Suppress("UNCHECKED_CAST") + return tmpl.makeId(BuiltinName("there")) as T + } + else -> null + } + } + override fun > adjustFilesAfterTranslation(files: MutableList) { + @Suppress("UNCHECKED_CAST") + files.add(tmpl.makeId(BuiltinName("a")) as T) + } + } + class TestAdjusterB : BackendAdjuster { + override fun > adjustConnectedCall(decl: TmpL.FunctionDeclaration, call: T): T? { + return call + } + override fun > adjustFilesAfterTranslation(files: MutableList) { + @Suppress("UNCHECKED_CAST") + files.add(tmpl.makeId(BuiltinName("b")) as T) + } + } + class TestAdjusterFactory(val adjuster: BackendAdjuster) : BackendAdjusterFactory { + override fun makeAdjuster(module: TmpL.Module, translator: Any): BackendAdjuster = adjuster + } + val comboFactory = TestAdjusterFactory(TestAdjusterA()) + .orElse(TestAdjusterFactory(TestAdjusterB())) + // Work on some tmpl. + val module = tmpl.module { + moduleFunction(BuiltinName("hi")) {} + } + val adjuster = comboFactory.makeAdjuster(module, Unit) + val function = module.topLevels.first() as TmpL.FunctionDeclaration + fun adjustCall(name: String): String { + val id = adjuster.adjustConnectedCall(function, tmpl.makeId(BuiltinName(name))) + return id!!.name.displayName + } + // The first adjuster only replaces name "_". The second always returns what it's given. + assertEquals("there", adjustCall("_")) + assertEquals("yall", adjustCall("yall")) + // Now see how we finalize, expecting reverse order. + assertContentEquals( + listOf("b", "a"), + buildList { adjuster.adjustFilesAfterTranslation(this) }.map { it.name.displayName }, + ) + } @Test fun backendOrganization() { - class NeedyBackendFactory(backendId: String, requiredBackendIds: List) : TestBackend.TestFactory() { + class NeedyBackendFactory( + backendId: String, + requiredBackendIds: List, + val adjusterFactories: Map = mapOf(), + ) : TestBackend.TestFactory() { override val backendId: BackendId = BackendId(backendId) override val backendMeta = super.backendMeta.copy( backendId = this.backendId, requiredBackendIds = requiredBackendIds, ) + override fun adjusterFactories(): Map { + return adjusterFactories + } } val missingBackendId = BackendId("missing") val needyFactory = NeedyBackendFactory("needy", listOf(TestBackend.backendId, missingBackendId)) val needlessFactory = NeedyBackendFactory("needless", listOf()) + val uselessAdjuster = object : BackendAdjuster {} + val uselessAdjusterFactory = object : BackendAdjusterFactory { + override fun makeAdjuster(module: TmpL.Module, translator: Any): BackendAdjuster = uselessAdjuster + } val needierFactory = NeedyBackendFactory( - "needier", - listOf(needyFactory.backendId, needlessFactory.backendId), + backendId = "needier", + requiredBackendIds = listOf(needyFactory.backendId, needlessFactory.backendId), + adjusterFactories = mapOf( + needyFactory.backendId to uselessAdjusterFactory, + // We don't directly require this, so we shouldn't be adjusting it. + TestBackend.backendId to uselessAdjusterFactory, + ), ) val aloofBackend = NeedyBackendFactory("aloof", listOf()) val requestedAloofFactory = NeedyBackendFactory("requested-aloof", listOf()) @@ -45,14 +114,27 @@ class BackendTest { aloofBackend, requestedAloofFactory, ).associateBy { it.backendId } - val missingBackendIds = mutableSetOf() + val errors = mutableSetOf() val organization = organizeBackends( backendIds = listOf(needierFactory.backendMeta.backendId, requestedAloofFactory.backendId), lookupFactory = { backends[it] }, - onMissingFactory = { missingBackendIds.add(it) }, + onError = { err -> errors.add(err) }, ) - assertEquals(setOf(missingBackendId), missingBackendIds) // Collections maintain order by default, so these should be reliable. + assertEquals( + setOf( + BackendOrganizationError( + kind = BackendOrganizationErrorKind.FactoryNotFound, + backendId = missingBackendId, + ), + BackendOrganizationError( + kind = BackendOrganizationErrorKind.AdjusterForUnrequiredBackend, + backendId = TestBackend.backendId, + sourceBackendId = needierFactory.backendId, + ), + ), + errors, + ) assertEquals( mapOf( needierFactory.backendId to setOf( @@ -77,6 +159,7 @@ class BackendTest { organization.backendBuckets, ) assertEquals(backends.keys - setOf(aloofBackend.backendId), organization.factoriesById.keys) + assertEquals(uselessAdjusterFactory, organization.adjusterFactories[needyFactory.backendId]) } @Test @@ -132,4 +215,11 @@ class BackendTest { Backend.defaultFilePathForSource(helloWorldLibraryConfig, moduleName, ".out"), ) } + + private val helloWorldLibraryConfig = LibraryConfiguration( + libraryName = DashedIdentifier.from("hello-world")!!, + libraryRoot = dirPath("a"), + supportedBackendList = emptyList(), + classifyTemperSource = ::defaultClassifyTemperSource, + ) } diff --git a/tooling/src/commonMain/kotlin/lang/temper/tooling/buildrun/Build.kt b/tooling/src/commonMain/kotlin/lang/temper/tooling/buildrun/Build.kt index c5cd9138..21912713 100644 --- a/tooling/src/commonMain/kotlin/lang/temper/tooling/buildrun/Build.kt +++ b/tooling/src/commonMain/kotlin/lang/temper/tooling/buildrun/Build.kt @@ -384,13 +384,15 @@ fun doOneBuild(build: Build): BuildResult { val backendOrganization = organizeBackends( backendIds = libraries.flatMap { it.first.supportedBackendList }.toSet(), lookupFactory = ::lookupFactory, - onMissingFactory = { backendId -> - if (backendId != interpBackendId) { + onError = { err -> + if (err.backendId != interpBackendId) { + // Ignore error kind for now, but that might be nice to add in the future. + // These errors should be rare. projectLogSink.log( Log.Error, MessageTemplate.BadBackend, unknownPos, - listOf(backendId), + listOf(err.backendId), ) } }, @@ -428,6 +430,7 @@ fun doOneBuild(build: Build): BuildResult { logSink = projectLogSink, dependencyResolver = dependencyResolver, config = harness.backendConfig, + adjusterFactory = backendOrganization.adjusterFactories[factory.backendId], ), ) }