1011 lines
43 KiB
Kotlin

/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import net.sergeych.lyng.Script.Companion.defaultImportManager
import net.sergeych.lyng.bridge.bind
import net.sergeych.lyng.bridge.bindObject
import net.sergeych.lyng.bytecode.CmdFunction
import net.sergeych.lyng.bytecode.CmdVm
import net.sergeych.lyng.bytecode.BytecodeLambdaCallable
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.serialization.ObjJsonClass
import net.sergeych.lyng.serialization.bindSerializationFormat
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.stdlib_included.complexLyng
import net.sergeych.lyng.stdlib_included.decimalLyng
import net.sergeych.lyng.stdlib_included.legacyDigestLyng
import net.sergeych.lyng.stdlib_included.matrixLyng
import net.sergeych.lyng.stdlib_included.observableLyng
import net.sergeych.lyng.stdlib_included.operatorsLyng
import net.sergeych.lyng.stdlib_included.rootLyng
import net.sergeych.lynon.ObjLynonClass
import net.sergeych.mp_tools.globalDefer
import kotlin.math.*
import kotlin.random.Random as KRandom
@Suppress("TYPE_INTERSECTION_AS_REIFIED_WARNING")
class Script(
override val pos: Pos,
private val statements: List<Statement> = emptyList(),
private val moduleSlotPlan: Map<String, Int> = emptyMap(),
private val moduleDeclaredNames: Set<String> = emptySet(),
private val importBindings: Map<String, ImportBinding> = emptyMap(),
private val importedModules: List<ImportBindingSource.Module> = emptyList(),
private val moduleBytecode: CmdFunction? = null,
// private val catchReturn: Boolean = false,
) : Statement() {
fun statements(): List<Statement> = statements
/** Compiler-resolved metadata for top-level function declarations in source order. */
fun resolvedFunctionMetadata(): List<ResolvedFunctionMetadata> =
statements.filterIsInstance<FunctionDeclStatement>().map { it.spec.resolvedMetadata }
/**
* Explicitly apply this script's import/module bindings to [scope] without executing the script.
* This is intended for embedding scenarios where the host owns scope lifecycle and wants
* script-specific imports to be a separate, opt-in preparation step.
*/
suspend fun importInto(scope: Scope, seedScope: Scope = scope): Scope {
prepareScriptScope(scope, seedScope)
return scope
}
/**
* Create a fresh raw module scope and apply this script's import/module bindings to it.
* If [seedScope] is provided, its import provider is reused and seed-bound imports resolve from it.
*/
suspend fun instantiateModule(seedScope: Scope? = null, pos: Pos = this.pos): ModuleScope {
val provider = seedScope?.currentImportProvider ?: defaultImportManager
val module = provider.newModuleAt(pos)
prepareScriptScope(module, seedScope ?: module)
return module
}
override suspend fun execute(scope: Scope): Obj {
scope.pos = pos
val execScope = resolveModuleScope(scope) ?: scope
val isModuleScope = execScope is ModuleScope
val shouldSeedModule = isModuleScope || execScope.thisObj === ObjVoid
val moduleTarget = (execScope as? ModuleScope) ?: execScope.parent as? ModuleScope ?: execScope
if (shouldSeedModule) {
seedModuleSlots(moduleTarget, scope)
}
moduleBytecode?.let { fn ->
if (execScope is ModuleScope) {
execScope.ensureModuleFrame(fn)
}
var execFrame: net.sergeych.lyng.bytecode.CmdFrame? = null
val result = CmdVm().execute(fn, execScope, scope.args) { frame, _ ->
execFrame = frame
seedModuleLocals(frame, moduleTarget, scope)
}
if (execScope !is ModuleScope) {
execFrame?.let { syncFrameLocalsToScope(it, execScope) }
}
return result
}
if (statements.isNotEmpty()) {
scope.raiseIllegalState("bytecode-only execution is required; missing module bytecode")
}
return ObjVoid
}
private suspend fun prepareScriptScope(scope: Scope, seedScope: Scope) {
if (importBindings.isNotEmpty() || importedModules.isNotEmpty()) {
seedImportBindings(scope, seedScope)
}
if (moduleSlotPlan.isNotEmpty()) {
installModuleSlotPlan(scope)
}
}
private suspend fun seedModuleSlots(scope: Scope, seedScope: Scope) {
if (importBindings.isEmpty() && importedModules.isEmpty()) return
seedImportBindings(scope, seedScope)
if (moduleSlotPlan.isNotEmpty()) {
installModuleSlotPlan(scope)
}
}
private fun installModuleSlotPlan(scope: Scope) {
for ((name, index) in moduleSlotPlan) {
if (scope.getSlotIndexOf(name) != null) continue
if (scope.hasSlotPlanConflict(mapOf(name to index))) {
val record = scope.objects[name]
?: scope.localBindings[name]
?: ObjRecord(ObjUnset, isMutable = true)
scope.allocateSlotFor(name, record)
} else {
scope.applySlotPlan(mapOf(name to index))
}
}
for (name in moduleSlotPlan.keys) {
val record = scope.objects[name] ?: scope.localBindings[name] ?: continue
scope.updateSlotFor(name, record)
}
}
private suspend fun seedModuleLocals(
frame: net.sergeych.lyng.bytecode.CmdFrame,
scope: Scope,
seedScope: Scope
) {
val localNames = frame.fn.localSlotNames
if (localNames.isEmpty()) return
val values = HashMap<String, Obj>()
val base = frame.fn.scopeSlotCount
for (i in localNames.indices) {
val name = localNames[i] ?: continue
if (moduleDeclaredNames.contains(name)) continue
val record = seedScope.getLocalRecordDirect(name) ?: findSeedRecord(seedScope, name) ?: continue
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) {
scope.resolve(record, name)
} else {
val raw = record.value
when (raw) {
is FrameSlotRef -> {
if (raw.refersTo(frame.frame, i)) {
raw.peekValue() ?: continue
} else if (seedScope !is ModuleScope) {
raw
} else {
raw.read()
}
}
is RecordSlotRef -> {
if (seedScope !is ModuleScope) raw else raw.read()
}
else -> raw
}
}
frame.setObjUnchecked(base + i, value)
}
}
private fun syncFrameLocalsToScope(frame: net.sergeych.lyng.bytecode.CmdFrame, scope: Scope) {
val localNames = frame.fn.localSlotNames
if (localNames.isEmpty()) return
for (i in localNames.indices) {
val name = localNames[i] ?: continue
val record = scope.getLocalRecordDirect(name) ?: scope.localBindings[name] ?: scope.objects[name] ?: continue
val value = frame.readLocalObj(i)
when (val current = record.value) {
is FrameSlotRef -> current.write(value)
is RecordSlotRef -> current.write(value)
else -> record.value = value
}
scope.updateSlotFor(name, record)
}
}
private fun importedBindingRecord(record: ObjRecord, source: Scope): ObjRecord =
record.copy(importedFrom = source)
private suspend fun seedImportBindings(scope: Scope, seedScope: Scope) {
val provider = scope.currentImportProvider
val importedModules = LinkedHashSet<ModuleScope>()
for (moduleRef in this.importedModules) {
importedModules.add(provider.prepareImport(moduleRef.pos, moduleRef.name, null))
}
for (module in importedModules) {
module.importInto(scope, null)
}
for ((name, binding) in importBindings) {
val sourceScope: Scope
val baseRecord = when (val source = binding.source) {
is ImportBindingSource.Module -> {
val module = provider.prepareImport(source.pos, source.name, null)
importedModules.add(module)
sourceScope = module
module.objects[binding.symbol]?.takeIf { it.visibility.isPublic }
?: scope.raiseSymbolNotFound("symbol ${source.name}.${binding.symbol} not found")
}
ImportBindingSource.Root -> {
sourceScope = provider.rootScope
provider.rootScope.objects[binding.symbol]?.takeIf { it.visibility.isPublic }
?: scope.raiseSymbolNotFound("symbol ${binding.symbol} not found")
}
ImportBindingSource.Seed -> {
sourceScope = seedScope
findSeedRecord(seedScope, binding.symbol)
?: scope.raiseSymbolNotFound("symbol ${binding.symbol} not found")
}
}
val record = importedBindingRecord(baseRecord, sourceScope)
if (name == "Exception" && record.value !is ObjClass) {
scope.updateSlotFor(name, ObjRecord(ObjException.Root, isMutable = false, importedFrom = sourceScope))
} else {
scope.updateSlotFor(name, record)
}
}
for (module in importedModules) {
for ((cls, map) in module.extensions) {
for ((symbol, record) in map) {
if (record.visibility.isPublic && record.importedFrom == null) {
scope.addExtension(cls, symbol, importedBindingRecord(record, module))
}
}
}
}
if (scope is ModuleScope) {
scope.importedModules = importedModules.toList()
}
}
private fun findSeedRecord(scope: Scope?, name: String): ObjRecord? {
var s = scope
var hops = 0
while (s != null && hops++ < 1024) {
s.objects[name]?.let { return it }
s.localBindings[name]?.let { return it }
s.getSlotIndexOf(name)?.let { idx ->
val rec = s.getSlotRecord(idx)
if (rec.value !== ObjUnset) return rec
}
s = s.parent
}
return null
}
private fun resolveModuleScope(scope: Scope): ModuleScope? {
return scope as? ModuleScope
}
internal fun debugStatements(): List<Statement> = statements
suspend fun execute() = execute(
defaultImportManager.newStdScope()
)
companion object {
private suspend fun ScopeFacade.numberToDouble(value: Obj): Double =
ObjDecimalSupport.toDoubleOrNull(value) ?: value.toDouble()
private suspend fun ScopeFacade.decimalAwareUnaryMath(
value: Obj,
exactDecimal: (suspend ScopeFacade.(Obj) -> Obj?)? = null,
fallback: (Double) -> Double
): Obj {
exactDecimal?.let { exact ->
exact(value)?.let { return it }
}
if (ObjDecimalSupport.isDecimalValue(value)) {
return ObjDecimalSupport.fromRealLike(this, value, fallback(numberToDouble(value)))
?: raiseIllegalState("failed to convert Real result back to Decimal")
}
return ObjReal(fallback(numberToDouble(value)))
}
private suspend fun ScopeFacade.decimalAwareRoundLike(
value: Obj,
exactDecimal: suspend ScopeFacade.(Obj) -> Obj?,
realFallback: (Double) -> Double
): Obj {
if (value is ObjInt) return value
exactDecimal(value)?.let { return it }
return ObjReal(realFallback(numberToDouble(value)))
}
private suspend fun ScopeFacade.decimalAwarePow(base: Obj, exponent: Obj): Obj {
ObjDecimalSupport.exactPow(this, base, exponent)?.let { return it }
if (ObjDecimalSupport.isDecimalValue(base) || ObjDecimalSupport.isDecimalValue(exponent)) {
return ObjDecimalSupport.fromRealLike(this, base, numberToDouble(base).pow(numberToDouble(exponent)))
?: ObjDecimalSupport.fromRealLike(this, exponent, numberToDouble(base).pow(numberToDouble(exponent)))
?: raiseIllegalState("failed to convert Real pow result back to Decimal")
}
return ObjReal(numberToDouble(base).pow(numberToDouble(exponent)))
}
/**
* Create new scope using a standard safe set of modules, using [defaultImportManager]. It is
* suspended as first time invocation requires compilation of standard library or other
* asynchronous initialization.
*/
suspend fun newScope(pos: Pos = Pos.builtIn) = defaultImportManager.newStdScope(pos)
internal val rootScope: Scope = Scope(null).apply {
ObjException.addExceptionsToContext(this)
addConst("Unset", ObjUnset)
addFn("print") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + toStringOf(a).value)
else print(toStringOf(a).value)
}
ObjVoid
}
addFn("println") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + toStringOf(a).value)
else print(toStringOf(a).value)
}
println()
ObjVoid
}
addFn("call") {
val callee = args.list.firstOrNull()
?: raiseError("call requires a callable as the first argument")
val rest = if (args.list.size > 1) {
Arguments(args.list.drop(1))
} else {
Arguments.EMPTY
}
call(callee, rest)
}
addFn("floor") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjDecimalSupport::exactFloor, ::floor)
}
addFn("ceil") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjDecimalSupport::exactCeil, ::ceil)
}
addFn("round") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjDecimalSupport::exactRound, ::round)
}
addFn("sin") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
}
addFn("cos") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cos)
}
addFn("tan") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tan)
}
addFn("asin") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asin)
}
addFn("acos") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acos)
}
addFn("atan") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atan)
}
addFn("sinh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sinh)
}
addFn("cosh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cosh)
}
addFn("tanh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tanh)
}
addFn("asinh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asinh)
}
addFn("acosh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acosh)
}
addFn("atanh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atanh)
}
addFn("exp") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::exp)
}
addFn("ln") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::ln)
}
addFn("log10") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log10)
}
addFn("log2") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log2)
}
addFn("pow") {
requireExactCount(2)
decimalAwarePow(args[0], args[1])
}
addItem(
"sqrt",
false,
ObjExternCallable.fromBridge {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sqrt)
},
recordType = ObjRecord.Type.Fun,
typeDecl = TypeDecl.Function(
receiver = null,
params = listOf(TypeDecl.TypeAny),
returnType = TypeDecl.Simple("Real", false),
nullable = false
)
)
addFn("abs") {
val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue)
else decimalAwareUnaryMath(x, ObjDecimalSupport::exactAbs) { it.absoluteValue }
}
addFnDoc(
"clamp",
doc = "Clamps the value within the specified range. If the value is outside the range, it is set to the nearest boundary. Respects inclusive/exclusive range ends.",
params = listOf(ParamDoc("value"), ParamDoc("range")),
moduleName = "lyng.stdlib"
) {
val value = requiredArg<Obj>(0)
val range = requiredArg<ObjRange>(1)
var result = value
if (range.start != null && !range.start.isNull) {
if (result.compareTo(requireScope(), range.start) < 0) {
result = range.start
}
}
if (range.end != null && !range.end.isNull) {
val cmp = range.end.compareTo(requireScope(), result)
if (range.isEndInclusive) {
if (cmp < 0) result = range.end
} else {
if (cmp <= 0) {
if (range.end is ObjInt) {
result = ObjInt.of(range.end.value - 1)
} else if (range.end is ObjChar) {
result = ObjChar((range.end.value.code - 1).toChar())
} else {
// For types where we can't easily find "previous" value (like Real),
// we just return the exclusive boundary as a fallback.
result = range.end
}
}
}
}
result
}
addVoidFn("assert") {
val cond = requiredArg<ObjBool>(0)
val message = if (args.size > 1)
": " + toStringOf(call(args[1])).value
else ""
if (!cond.value == true)
raiseAssertionFailed("Assertion failed$message")
}
fun unwrapCompareArg(value: Obj): Obj {
val resolved = when (value) {
is FrameSlotRef -> value.read()
is RecordSlotRef -> value.read()
else -> value
}
return if (resolved === ObjUnset.objClass) ObjUnset else resolved
}
addVoidFn("assertEquals") {
val a = unwrapCompareArg(requiredArg(0))
val b = unwrapCompareArg(requiredArg(1))
if (a.compareTo(requireScope(), b) != 0) {
raiseAssertionFailed("Assertion failed: ${inspect(a)} == ${inspect(b)}")
}
}
// alias used in tests
addVoidFn("assertEqual") {
val a = unwrapCompareArg(requiredArg(0))
val b = unwrapCompareArg(requiredArg(1))
if (a.compareTo(requireScope(), b) != 0)
raiseAssertionFailed("Assertion failed: ${inspect(a)} == ${inspect(b)}")
}
addVoidFn("assertNotEquals") {
val a = unwrapCompareArg(requiredArg(0))
val b = unwrapCompareArg(requiredArg(1))
if (a.compareTo(requireScope(), b) == 0)
raiseAssertionFailed("Assertion failed: ${inspect(a)} != ${inspect(b)}")
}
addFnDoc(
"assertThrows",
doc = """
Asserts that the provided code block throws an exception, with or without exception:
```lyng
assertThrows { /* ode */ }
assertThrows(IllegalArgumentException) { /* code */ }
```
If an expected exception class is provided,
it checks that the thrown exception is of that class. If no expected class is provided, any exception
will be accepted.
""".trimIndent()
) {
val code: Obj
val expectedClass: ObjClass?
when (args.size) {
1 -> {
code = requiredArg<Obj>(0)
expectedClass = null
}
2 -> {
code = requiredArg<Obj>(1)
expectedClass = requiredArg<ObjClass>(0)
}
else -> raiseIllegalArgument("Expected 1 or 2 arguments, got ${args.size}")
}
val result = try {
call(code)
null
} catch (e: ExecutionError) {
e.errorObject
} catch (_: ScriptError) {
ObjNull
}
if (result == null) raiseAssertionFailed("Expected exception but nothing was thrown")
expectedClass?.let {
if (!result.isInstanceOf(it)) {
val actual = if (result is ObjException) result.exceptionClass else result.objClass
raiseError("Expected $it, got $actual")
}
}
result
}
addFn("dynamic", callSignature = CallSignature(tailBlockReceiverType = "DelegateContext")) {
ObjDynamic.create(requireScope(), requireOnlyArg())
}
val root = this
val mathClass = ObjClass("Math").apply {
addFn("sqrt") {
ObjReal(sqrt(args.firstAndOnly().toDouble()))
}
}
addItem("Math", false, ObjInstance(mathClass).apply {
instanceScope = Scope(root, thisObj = this)
})
addFn("require") {
val condition = requiredArg<ObjBool>(0)
if (!condition.value) {
var message = args.list.getOrNull(1)
if (message is Obj && message.objClass == Statement.type) message = call(message)
raiseIllegalArgument(message?.toString() ?: "requirement not met")
}
ObjVoid
}
addFn("check") {
val condition = requiredArg<ObjBool>(0)
if (!condition.value) {
var message = args.list.getOrNull(1)
if (message is Obj && message.objClass == Statement.type) message = call(message)
raiseIllegalState(message?.toString() ?: "check failed")
}
ObjVoid
}
addFn("traceScope") {
trace(args.getOrNull(0)?.toString() ?: "")
ObjVoid
}
addFn("run") {
call(requireOnlyArg())
}
addFn("cached") {
val builder = requireOnlyArg<Obj>()
val capturedScope = this
var calculated = false
var cachedValue: Obj = ObjVoid
net.sergeych.lyng.obj.ObjExternCallable.fromBridge {
if (!calculated) {
cachedValue = capturedScope.call(builder)
calculated = true
}
cachedValue
}
}
addVoidFn("delay") {
val a = args.firstAndOnly()
when (a) {
is ObjInt -> delay(a.value)
is ObjReal -> delay((a.value * 1000).roundToLong())
is ObjDuration -> delay(a.duration)
else -> raiseIllegalArgument("Expected Int, Real or Duration, got ${inspect(a)}")
}
}
addConst("Object", rootObjectType)
addConst("Real", ObjReal.type)
addConst("String", ObjString.type)
addConst("Int", ObjInt.type)
addConst("Bool", ObjBool.type)
addConst("Char", ObjChar.type)
addConst("List", ObjList.type)
addConst("ImmutableList", ObjImmutableList.type)
addConst("Set", ObjSet.type)
addConst("ImmutableSet", ObjImmutableSet.type)
addConst("Range", ObjRange.type)
addConst("Map", ObjMap.type)
addConst("ImmutableMap", ObjImmutableMap.type)
addConst("MapEntry", ObjMapEntry.type)
@Suppress("RemoveRedundantQualifierName")
addConst("Callable", Statement.type)
// interfaces
addConst("Iterable", ObjIterable)
addConst("Collection", ObjCollection)
addConst("Iterator", ObjIterator)
addConst("Array", ObjArray)
addConst("RingBuffer", ObjRingBuffer.type)
addConst("Class", ObjClassType)
addConst("Deferred", ObjDeferred.type)
addConst("CompletableDeferred", ObjCompletableDeferred.type)
addConst("Mutex", ObjMutex.type)
addConst("Channel", ObjChannel.type)
addConst("Flow", ObjFlow.type)
addConst("FlowBuilder", ObjFlowBuilder.type)
addConst("Delegate", ObjDynamic.type)
addConst("DelegateContext", ObjDynamicContext.type)
addConst("Regex", ObjRegex.type)
addConst("RegexMatch", ObjRegexMatch.type)
addConst("MapEntry", ObjMapEntry.type)
addFn("launch") {
val rawCallable = requireOnlyArg<Obj>()
val currentScope = requireScope()
// Freeze non-module lexical state at launch time so each coroutine gets
// its own view of loop locals and other transient frame values.
val captured = if (currentScope is ModuleScope) currentScope else currentScope.snapshotForClosure()
val callable = (rawCallable as? BytecodeLambdaCallable)?.freezeForLaunch(captured) ?: rawCallable
val session = EvalSession.currentOrNull()
val deferred = if (session != null) {
session.launchTrackedDeferred {
ScopeBridge(captured).call(callable)
}
} else {
globalDefer {
ScopeBridge(captured).call(callable)
}
}
ObjDeferred(deferred)
}
addFn("yield") {
yield()
ObjVoid
}
addFn("flow", callSignature = CallSignature(tailBlockReceiverType = "FlowBuilder")) {
// important is: current context contains closure often used in call;
// we'll need it for the producer
ObjFlow(requireOnlyArg<Obj>(), requireScope(), EvalSession.currentOrNull())
}
val pi = ObjReal(PI)
addConstDoc(
name = "π",
value = pi,
doc = "The mathematical constant pi (π).",
type = type("lyng.Real")
)
getOrCreateNamespace("Math").apply {
fun ensureFn(name: String, fn: suspend ScopeFacade.() -> Obj) {
if (members.containsKey(name)) return
addFn(name, code = fn)
}
addConstDoc(
name = "PI",
value = pi,
doc = "The mathematical constant pi (π) in the Math namespace.",
type = type("lyng.Real")
)
ensureFn("floor") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjDecimalSupport::exactFloor, ::floor)
}
ensureFn("ceil") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjDecimalSupport::exactCeil, ::ceil)
}
ensureFn("round") {
val x = args.firstAndOnly()
decimalAwareRoundLike(x, ObjDecimalSupport::exactRound, ::round)
}
ensureFn("sin") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sin)
}
ensureFn("cos") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cos)
}
ensureFn("tan") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tan)
}
ensureFn("asin") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asin)
}
ensureFn("acos") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acos)
}
ensureFn("atan") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atan)
}
ensureFn("sinh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sinh)
}
ensureFn("cosh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::cosh)
}
ensureFn("tanh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::tanh)
}
ensureFn("asinh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::asinh)
}
ensureFn("acosh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::acosh)
}
ensureFn("atanh") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::atanh)
}
ensureFn("exp") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::exp)
}
ensureFn("ln") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::ln)
}
ensureFn("log10") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log10)
}
ensureFn("log2") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::log2)
}
ensureFn("pow") {
requireExactCount(2)
decimalAwarePow(args[0], args[1])
}
ensureFn("sqrt") {
decimalAwareUnaryMath(args.firstAndOnly(), fallback = ::sqrt)
}
ensureFn("abs") {
val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue)
else decimalAwareUnaryMath(x, ObjDecimalSupport::exactAbs) { it.absoluteValue }
}
}
}
private fun seededRandomFromThis(scope: Scope, thisObj: Obj): KRandom {
val instance = thisObj as? ObjInstance
?: scope.raiseIllegalState("SeededRandom method requires instance receiver")
val stored = instance.kotlinInstanceData
if (stored is KRandom) return stored
return KRandom.Default.also { instance.kotlinInstanceData = it }
}
private suspend fun sampleRangeValue(scope: Scope, random: KRandom, range: ObjRange): Obj {
if (range.start == null || range.start.isNull || range.end == null || range.end.isNull) {
scope.raiseIllegalArgument("Random.next(range) requires a finite range")
}
val start = range.start
val end = range.end
// Real ranges without explicit step are sampled continuously.
if (!range.hasExplicitStep &&
start is Numeric &&
end is Numeric &&
(start !is ObjInt || end !is ObjInt)
) {
val from = start.doubleValue
val to = end.doubleValue
if (from > to || (!range.isEndInclusive && from == to)) {
scope.raiseIllegalArgument("Random.next(range) got an empty numeric range")
}
if (from == to) return ObjReal(from)
val upperExclusive = if (range.isEndInclusive) to.nextUp() else to
if (upperExclusive <= from) {
scope.raiseIllegalArgument("Random.next(range) got an empty numeric range")
}
return ObjReal(random.nextDouble(from, upperExclusive))
}
// Discrete sampling for stepped ranges and integer/char ranges.
var picked: Obj? = null
var count = 0L
range.enumerate(scope) { value ->
count += 1
if (random.nextLong(count) == 0L) {
picked = value
}
true
}
if (count <= 0L || picked == null) {
scope.raiseIllegalArgument("Random.next(range) got an empty range")
}
return picked
}
val defaultImportManager: ImportManager by lazy {
ImportManager(rootScope, SecurityManager.allowAll).apply {
addPackage("lyng.stdlib") { module ->
module.eval(Source("lyng.stdlib", rootLyng))
ObjKotlinIterator.bindTo(module.requireClass("KotlinIterator"))
val seededRandomClass = module.requireClass("SeededRandom")
module.bind("SeededRandom") {
init { data = KRandom.Default }
addFun("nextInt") {
val rnd = seededRandomFromThis(requireScope(), thisObj)
ObjInt.of(rnd.nextInt().toLong())
}
addFun("nextFloat") {
val rnd = seededRandomFromThis(requireScope(), thisObj)
ObjReal(rnd.nextDouble())
}
addFun("next") {
val rnd = seededRandomFromThis(requireScope(), thisObj)
val range = requiredArg<ObjRange>(0)
sampleRangeValue(requireScope(), rnd, range)
}
}
module.bindObject("Random") {
addFun("nextInt") {
ObjInt.of(KRandom.Default.nextInt().toLong())
}
addFun("nextFloat") {
ObjReal(KRandom.Default.nextDouble())
}
addFun("next") {
val range = requiredArg<ObjRange>(0)
sampleRangeValue(requireScope(), KRandom.Default, range)
}
addFun("seeded") {
val seed = requiredArg<ObjInt>(0).value.toInt()
val instance = call(seededRandomClass) as? ObjInstance
?: requireScope().raiseIllegalState("SeededRandom() did not return an object instance")
instance.kotlinInstanceData = KRandom(seed)
instance
}
}
}
addPackage("lyng.observable") { module ->
module.addConst("Observable", ObjObservable)
module.addConst("Subscription", ObjSubscription.type)
module.addConst("ListChange", ObjListChange.type)
module.addConst("ListSet", ObjListSetChange.type)
module.addConst("ListInsert", ObjListInsertChange.type)
module.addConst("ListRemove", ObjListRemoveChange.type)
module.addConst("ListClear", ObjListClearChange.type)
module.addConst("ListReorder", ObjListReorderChange.type)
module.addConst("ObservableList", ObjObservableList.type)
module.addConst("ChangeRejectionException", ObjChangeRejectionExceptionClass)
module.eval(Source("lyng.observable", observableLyng))
}
addPackage("lyng.operators") { module ->
module.eval(Source("lyng.operators", operatorsLyng))
module.bindObject("OperatorInterop") {
addFun("register") {
val leftClass = requiredArg<ObjClass>(0)
val rightClass = requiredArg<ObjClass>(1)
val commonClass = requiredArg<ObjClass>(2)
val operators = requiredArg<ObjList>(3).list.map { value ->
val entry = value as? ObjEnumEntry
?: requireScope().raiseIllegalArgument(
"OperatorInterop.register expects BinaryOperator enum entries"
)
entry.name.value
}
OperatorInteropRegistry.register(
leftClass = leftClass,
rightClass = rightClass,
commonClass = commonClass,
operatorNames = operators,
leftToCommon = args[4],
rightToCommon = args[5]
)
ObjVoid
}
}
}
addPackage("lyng.decimal") { module ->
module.eval(Source("lyng.decimal", decimalLyng))
ObjDecimalSupport.bindTo(module)
}
addPackage("lyng.matrix") { module ->
module.eval(Source("lyng.matrix", matrixLyng))
ObjMatrixSupport.bindTo(module)
}
addPackage("lyng.complex") { module ->
module.eval(Source("lyng.complex", complexLyng))
ObjComplexSupport.bindTo(module)
}
addPackage("lyng.legacy_digest") { module ->
module.eval(Source("lyng.legacy_digest", legacyDigestLyng))
module.bindObject("LegacyDigest") {
addFun("sha1") {
val data = requiredArg<Obj>(0)
val bytes = when (data) {
is ObjBuffer -> data.byteArray.toByteArray()
else -> data.toString().encodeToByteArray()
}
ObjString(LegacyDigest.sha1Hex(bytes))
}
}
}
addPackage("lyng.buffer") {
it.addConstDoc(
name = "Buffer",
value = ObjBuffer.type,
doc = "Immutable sequence of bytes. Use for binary data and IO.",
type = type("lyng.Class")
)
it.addConstDoc(
name = "MutableBuffer",
value = ObjMutableBuffer.type,
doc = "Mutable byte buffer. Supports in-place modifications.",
type = type("lyng.Class")
)
}
addPackage("lyng.serialization") {
it.bindSerializationFormat(
ObjLynonClass,
doc = "Lynon serialization utilities: encode/decode data structures to a portable binary format."
)
it.bindSerializationFormat(
ObjJsonClass,
doc = "Universal JSON serialization utilities with bidirectional Lyng object support."
)
}
addPackage("lyng.time") {
it.addConstDoc(
name = "Instant",
value = ObjInstant.type,
doc = "Point in time (epoch-based).",
type = type("lyng.Class")
)
it.addConstDoc(
name = "Date",
value = ObjDate.type,
doc = "Calendar date without time-of-day or time zone.",
type = type("lyng.Class")
)
it.addConstDoc(
name = "DateTime",
value = ObjDateTime.type,
doc = "Point in time in a specific time zone.",
type = type("lyng.Class")
)
it.addConstDoc(
name = "Duration",
value = ObjDuration.type,
doc = "Time duration with millisecond precision.",
type = type("lyng.Class")
)
it.addVoidFnDoc(
"delay",
doc = "Suspend for the given time. Accepts Duration, Int milliseconds, or Real seconds."
) {
val a = args.firstAndOnly()
when (a) {
is ObjInt -> delay(a.value)
is ObjReal -> delay((a.value * 1000).roundToLong())
is ObjDuration -> delay(a.duration)
else -> raiseIllegalArgument("Expected Duration, Int or Real, got ${inspect(a)}")
}
}
}
}
}
}
}