diff --git a/skainet-bom/build.gradle.kts b/skainet-bom/build.gradle.kts index 9d8ba044..828b25e4 100644 --- a/skainet-bom/build.gradle.kts +++ b/skainet-bom/build.gradle.kts @@ -15,7 +15,8 @@ dependencies { // Core language module api(project(":skainet-lang:skainet-lang-core")) - // CPU backend + // Backend abstraction + CPU backend + api(project(":skainet-backends:skainet-backend-api")) api(project(":skainet-backends:skainet-backend-cpu")) // IO modules diff --git a/skainet-compile/skainet-compile-hlo/src/commonMain/kotlin/sk/ainet/compile/hlo/StableHloConverterFactory.kt b/skainet-compile/skainet-compile-hlo/src/commonMain/kotlin/sk/ainet/compile/hlo/StableHloConverterFactory.kt index 81c42dc7..cf6bd46f 100644 --- a/skainet-compile/skainet-compile-hlo/src/commonMain/kotlin/sk/ainet/compile/hlo/StableHloConverterFactory.kt +++ b/skainet-compile/skainet-compile-hlo/src/commonMain/kotlin/sk/ainet/compile/hlo/StableHloConverterFactory.kt @@ -9,6 +9,7 @@ import sk.ainet.compile.hlo.converters.MathOperationsConverter import sk.ainet.compile.hlo.converters.NeuralNetOperationsConverter import sk.ainet.compile.hlo.converters.ReductionOperationsConverter import sk.ainet.compile.hlo.converters.ShapeOperationsConverter +import kotlin.jvm.JvmStatic /** * Factory for creating StableHLO converters with default configurations. @@ -21,6 +22,7 @@ public object StableHloConverterFactory { /** * Create a converter with basic operations support (add, matmul, relu) */ + @JvmStatic public fun createBasic(): StableHloConverter { val registry = StableHloOperationRegistry() val typeMapper = TypeMapper() @@ -57,6 +59,7 @@ public object StableHloConverterFactory { /** * Create a converter with extended operations support */ + @JvmStatic public fun createExtended(): StableHloConverter { val registry = StableHloOperationRegistry() val typeMapper = TypeMapper() @@ -96,6 +99,7 @@ public object StableHloConverterFactory { /** * Create a converter without validation (for performance) */ + @JvmStatic public fun createFast(): StableHloConverter { val registry = StableHloOperationRegistry() val typeMapper = TypeMapper() @@ -115,6 +119,8 @@ public object StableHloConverterFactory { /** * Create a custom converter with the provided components */ + @JvmStatic + @kotlin.jvm.JvmOverloads public fun createCustom( registry: StableHloOperationRegistry, typeMapper: TypeMapper = TypeMapper(), diff --git a/skainet-io/skainet-io-core/src/commonMain/kotlin/sk/ainet/io/tokenizer/TokenizerFactory.kt b/skainet-io/skainet-io-core/src/commonMain/kotlin/sk/ainet/io/tokenizer/TokenizerFactory.kt index e5b1b532..0874696e 100644 --- a/skainet-io/skainet-io-core/src/commonMain/kotlin/sk/ainet/io/tokenizer/TokenizerFactory.kt +++ b/skainet-io/skainet-io-core/src/commonMain/kotlin/sk/ainet/io/tokenizer/TokenizerFactory.kt @@ -3,6 +3,7 @@ package sk.ainet.io.tokenizer import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import kotlin.jvm.JvmStatic /** * Selects the right [Tokenizer] implementation for a model. @@ -33,6 +34,7 @@ public object TokenizerFactory { * `ggufModelMetadata.rawFields` — this keeps `skainet-io-core` free of a * dependency on `skainet-io-gguf`. */ + @JvmStatic public fun fromGguf(fields: Map): Tokenizer { val model = (fields["tokenizer.ggml.model"] as? String)?.lowercase() ?: throw UnsupportedTokenizerException( @@ -57,6 +59,7 @@ public object TokenizerFactory { * to [QwenByteLevelBpeTokenizer]; `"Unigram"` (SentencePiece) and * `"WordPiece"` currently throw. */ + @JvmStatic public fun fromTokenizerJson(json: String): Tokenizer { val root = JSON.parseToJsonElement(json).jsonObject val modelType = root["model"]?.jsonObject?.get("type")?.jsonPrimitive?.content diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/TensorSpecEncoding.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/TensorSpecEncoding.kt index 9cc4e620..3be531be 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/TensorSpecEncoding.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/ops/TensorSpecEncoding.kt @@ -1,8 +1,11 @@ +@file:JvmName("TensorSpecs") + package sk.ainet.lang.tensor.ops import sk.ainet.lang.tensor.data.TensorData import sk.ainet.lang.tensor.storage.PackedBlockStorage import sk.ainet.lang.tensor.storage.TensorEncoding +import kotlin.jvm.JvmName /** * Metadata key used to carry a [TensorEncoding] on a [TensorSpec]. diff --git a/skainet-test/skainet-test-java/build.gradle.kts b/skainet-test/skainet-test-java/build.gradle.kts index 7cacda18..8ad1e39a 100644 --- a/skainet-test/skainet-test-java/build.gradle.kts +++ b/skainet-test/skainet-test-java/build.gradle.kts @@ -12,6 +12,12 @@ dependencies { testImplementation(project(":skainet-lang:skainet-lang-core")) testImplementation(project(":skainet-backends:skainet-backend-cpu")) testImplementation(project(":skainet-data:skainet-data-simple")) + // 0.19.0 Java consumption surface: converter factory, tokenizer + // factory, and the TensorSpec encoding helper facade. Tested in + // ReleaseApiJavaTest so a Java consumer of the upcoming release + // has a reference invocation pattern for each. + testImplementation(project(":skainet-compile:skainet-compile-hlo")) + testImplementation(project(":skainet-io:skainet-io-core")) } tasks.test { diff --git a/skainet-test/skainet-test-java/src/test/java/sk/ainet/java/ReleaseApiJavaTest.java b/skainet-test/skainet-test-java/src/test/java/sk/ainet/java/ReleaseApiJavaTest.java new file mode 100644 index 00000000..2b7446f0 --- /dev/null +++ b/skainet-test/skainet-test-java/src/test/java/sk/ainet/java/ReleaseApiJavaTest.java @@ -0,0 +1,117 @@ +package sk.ainet.java; + +import org.junit.jupiter.api.Test; + +import sk.ainet.compile.hlo.StableHloConverter; +import sk.ainet.compile.hlo.StableHloConverterFactory; +import sk.ainet.io.tokenizer.TokenizerFactory; +import sk.ainet.io.tokenizer.UnsupportedTokenizerException; +import sk.ainet.lang.tensor.ops.TensorSpec; +import sk.ainet.lang.tensor.ops.TensorSpecs; +import sk.ainet.lang.tensor.storage.TensorEncoding; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Java consumer smoke tests for the Kotlin surfaces polished in the + * 0.19.0 release for Java-first-citizenship (#400). Each test is + * deliberately close to the call sites a Java consumer of the BOM + * would write so the patterns are self-documenting. + * + * If any of these lose their clean static form on a future Kotlin + * refactor (e.g. `@JvmStatic` dropped), the tests fail at compile + * time rather than at bytecode-verification time in production. + */ +class ReleaseApiJavaTest { + + // --- StableHloConverterFactory ----------------------------------------- + + /** + * The converter factory must be reachable via the idiomatic + * static form, not through the Kotlin object's INSTANCE marker. + * Written as a compile-time smoke test — if someone drops the + * @JvmStatic annotations this fails to compile before any + * assertion runs. + */ + @Test + void stableHloConverterFactoryIsStatic() { + StableHloConverter basic = StableHloConverterFactory.createBasic(); + StableHloConverter extended = StableHloConverterFactory.createExtended(); + StableHloConverter fast = StableHloConverterFactory.createFast(); + + assertNotNull(basic, "createBasic() must return a non-null converter"); + assertNotNull(extended, "createExtended() must return a non-null converter"); + assertNotNull(fast, "createFast() must return a non-null converter"); + } + + // --- TokenizerFactory -------------------------------------------------- + + /** + * TokenizerFactory's static form is the entry point Java consumers + * hit when they load a GGUF or HuggingFace tokenizer.json. The + * call shape must stay clean across releases. + * + * We pass an empty GGUF field map and expect an + * UnsupportedTokenizerException — the point is to prove the + * factory is dispatched via static, not to actually tokenize. + */ + @Test + void tokenizerFactoryFromGgufIsStatic() { + Map emptyFields = Collections.emptyMap(); + assertThrows( + UnsupportedTokenizerException.class, + () -> TokenizerFactory.fromGguf(emptyFields), + "empty GGUF metadata map must trip UnsupportedTokenizerException" + ); + } + + @Test + void tokenizerFactoryFromTokenizerJsonIsStatic() { + String emptyJson = "{}"; + assertThrows( + UnsupportedTokenizerException.class, + () -> TokenizerFactory.fromTokenizerJson(emptyJson), + "tokenizer.json with no model.type must trip UnsupportedTokenizerException" + ); + } + + // --- TensorSpecs (JvmName of TensorSpecEncoding.kt) -------------------- + + /** + * The TensorEncoding accessor helpers live on + * skainet-lang-core/.../ops/TensorSpecEncoding.kt, which now + * compiles to a class named TensorSpecs (via @file:JvmName). + * Java callers access read / copy via static-method syntax. + */ + @Test + void tensorSpecsEncodingHelpers() { + TensorSpec bare = new TensorSpec( + /* name= */ "w", + /* shape= */ List.of(8, 4), + /* dtype= */ "FP32", + /* requiresGrad= */ false, + /* metadata= */ Collections.emptyMap() + ); + + // Reader: an un-annotated spec has a null encoding. + assertNull(TensorSpecs.getTensorEncoding(bare), + "a fresh TensorSpec must have no tensorEncoding"); + + // Setter: returns a copy with the encoding attached. + TensorSpec annotated = TensorSpecs.withTensorEncoding( + bare, TensorEncoding.Q8_0.INSTANCE); + assertNotNull(TensorSpecs.getTensorEncoding(annotated), + "annotated spec must have a non-null tensorEncoding"); + assertSame(TensorEncoding.Q8_0.INSTANCE, + TensorSpecs.getTensorEncoding(annotated), + "annotated spec must carry the encoding we set"); + + // The original is unchanged — data-class copy semantics. + assertNull(TensorSpecs.getTensorEncoding(bare), + "withTensorEncoding must not mutate the source spec"); + } +}