diff --git a/AGENTS.md b/AGENTS.md index fe03481..fa74f8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,3 +6,17 @@ - If you need a wrapper for delegated properties, check for `getValue` explicitly and return a concrete `Statement` object when missing; avoid `onNotFoundResult` lambdas. - If wasmJs browser tests hang, first run `:lynglib:wasmJsNodeTest` and look for wasm compilation errors; hangs usually mean module instantiation failed. - Do not increase test timeouts to mask wasm generation errors; fix the invalid IR instead. + +## Type inference notes (notes/new_lyng_type_system_spec.md) +- Nullability is Kotlin-style: `T` non-null, `T?` nullable, `!!` asserts non-null. +- `void` is a singleton of class `Void` (syntax sugar for return type). +- Object members are always allowed even on unknown types; non-Object members require explicit casts. Remove `inspect` from Object and use `toInspectString()` instead. +- Type expression checks: `x is T` is value instance check; `T1 is T2` is type-subset; `A in T` means `A` is subset of `T`; `==` is structural type equality. +- Type aliases: `type Name = TypeExpr` (generic allowed) expand to their underlying type expressions; no nominal distinctness. +- Do not reintroduce bytecode fallback opcodes (e.g., `GET_NAME`, `EVAL_*`, `CALL_FALLBACK`) or runtime name-resolution fallbacks; all symbol resolution must stay compile-time only. + +## Bytecode frame-first migration plan +- Treat frame slots as the only storage for locals/temps by default; avoid pre-creating scope slot mappings for compiled functions. +- Create closure references only when a capture is detected; use a direct frame+slot reference (foreign slot ref) instead of scope slots. +- Keep Scope as a lazy reflection facade: resolve name -> slot only on demand for Kotlin interop (no eager name mapping on every call). +- Avoid PUSH_SCOPE/POP_SCOPE in bytecode for loops/functions unless dynamic name access or Kotlin reflection is requested. diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d06dd..0b56322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,31 @@ -## Changelog +## 1.5.0-SNAPSHOT -### Unreleased +### Language Features +- Added `return` statement with local and non-local exit support (`return@label`). +- Support for `abstract` classes, methods, and variables. +- Introduced `interface` as a synonym for `abstract class`. +- Multiple Inheritance (MI) completed and enabled by default (C3 MRO). +- Class properties with custom accessors (`get`, `set`). +- Restricted setter visibility (`private set`, `protected set`). +- Late-initialized `val` fields in classes with `Unset` protection. +- Named arguments (`name: value`) and named splats (`...Map`). +- Assign-if-null operator `?=`. +- Refined `protected` visibility rules and `closed` modifier. +- Transient attribute `@Transient` for serialization and equality. +- Unified Delegation model for `val`, `var`, and `fun`. +- Singleton objects (`object`) and object expressions. + +### Standard Library +- Added `with(self, block)` for scoped execution. +- Added `clamp()` function and extension. +- Improved `Exception` and `StackTraceEntry` reporting. + +### Tooling and IDE +- **CLI**: Added `fmt` as a first-class subcommand for code formatting. +- **IDEA Plugin**: Lightweight autocompletion (experimental), improved docs, and Grazie integration. +- **Highlighters**: Updated TextMate bundle and website highlighters for new syntax. + +### Detailed Changes: - Language: Refined `protected` visibility rules - Ancestor classes can now access `protected` members of their descendants, provided the ancestor also defines or inherits a member with the same name (indicating an override of a member known to the ancestor). @@ -109,16 +134,6 @@ - Documentation updated (docs/OOP.md and tutorial quick-start) to reflect MI with active C3 MRO. -Notes: -- Existing single-inheritance code continues to work; resolution reduces to the single base. -- If code previously relied on non-deterministic parent set iteration, C3 MRO provides a predictable order; disambiguate explicitly if needed using `this@Type`/casts. - -# Changelog - -All notable changes to this project will be documented in this file. - -## Unreleased - - CLI: Added `fmt` as a first-class Clikt subcommand. - Default behavior: formats files to stdout (no in-place edits by default). - Options: diff --git a/LYNG_AI_SPEC.md b/LYNG_AI_SPEC.md index 63a13ca..947e77c 100644 --- a/LYNG_AI_SPEC.md +++ b/LYNG_AI_SPEC.md @@ -1,4 +1,4 @@ -# Lyng Language AI Specification (V1.3) +# Lyng Language AI Specification (V1.5.0-SNAPSHOT) High-density specification for LLMs. Reference this for all Lyng code generation. @@ -19,6 +19,7 @@ High-density specification for LLMs. Reference this for all Lyng code generation - **Equality**: `==` (equals), `!=` (not equals), `===` (ref identity), `!==` (ref not identity). - **Comparison**: `<`, `>`, `<=`, `>=`, `<=>` (shuttle/spaceship, returns -1, 0, 1). - **Destructuring**: `val [a, b, rest...] = list`. Supports nested `[a, [b, c]]` and splats. +- **Compile-Time Resolution Only**: All names/members must resolve at compile time. No runtime name lookup or fallback opcodes. ## 2. Object-Oriented Programming (OOP) - **Multiple Inheritance**: Supported with **C3 MRO** (Python-style). Diamond-safe. @@ -37,6 +38,15 @@ High-density specification for LLMs. Reference this for all Lyng code generation - **Disambiguation**: `this@Base.member()` or `(obj as Base).member()`. `as` returns a qualified view. - **Abstract/Interface**: `interface` is a synonym for `abstract class`. Both support state and constructors. - **Extensions**: `fun Class.ext()` or `val Class.ext get = ...`. Scope-isolated. +- **Member Access**: Object members (`toString`, `toInspectString`, `let`, `also`, `apply`, `run`) are allowed on unknown types; all other members require a statically known receiver type or explicit cast. + +## 2.1 Type System (2026) +- **Root Type**: Everything is an `Object` (root of the hierarchy). +- **Nullability**: Non-null by default (`T`), nullable with `T?`, `!!` asserts non-null. +- **Untyped params**: `fun foo(x)` -> `x: Object`, `fun foo(x?)` -> `x: Object?`. +- **Untyped vars**: `var x` is `Unset` until first assignment locks the type. +- **Inference**: List/map literals infer union element types; empty list is `List`, empty map is `{:}`. +- **Generics**: Bounds with `T: A & B` or `T: A | B`; variance uses `out`/`in`. ## 3. Delegation (`by`) Unified model for `val`, `var`, and `fun`. @@ -63,11 +73,11 @@ Delegate Methods: - **Collections**: `List` ( `[a, b]` ), `Map` ( `Map(k => v)` ), `Set` ( `Set(a, b)` ). `MapEntry` ( `k => v` ). ## 5. Patterns & Shorthands -- **Map Literals**: `{ key: value, identifier: }` (identifier shorthand `x:` is `x: x`). Empty map is `Map()`. +- **Map Literals**: `{ key: value, identifier: }` (identifier shorthand `x:` is `x: x`). Empty map is `{:}`. - **Named Arguments**: `fun(y: 10, x: 5)`. Shorthand: `Point(x:, y:)`. - **Varargs & Splats**: `fun f(args...)`, `f(...otherList)`. - **Labels**: `loop@ for(x in list) { if(x == 0) break@loop }`. -- **Dynamic**: `val d = dynamic { get { name -> ... } }` allows `d.anyName`. +- **Dynamic**: `val d = dynamic { get { name -> ... } }` allows `d.anyName` via explicit dynamic handler (not implicit fallback). ## 6. Operators & Methods to Overload | Op | Method | Op | Method | diff --git a/README.md b/README.md index cb79d83..02c5671 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,16 @@ Point(x:, y:).dist() //< 5 fun swapEnds(first, args..., last, f) { f( last, ...args, first) } + +class A { + class B(x?) + object Inner { val foo = "bar" } + enum E* { One, Two } +} +val ab = A.B() +assertEquals(ab.x, null) +assertEquals(A.Inner.foo, "bar") +assertEquals(A.One, A.E.One) ``` - extremely simple Kotlin integration on any platform (JVM, JS, WasmJS, Lunux, MacOS, iOS, Windows) @@ -38,6 +48,7 @@ fun swapEnds(first, args..., last, f) { - [Language home](https://lynglang.com) - [introduction and tutorial](docs/tutorial.md) - start here please +- [What's New in 1.5](docs/whats_new_1_5.md) - [Testing and Assertions](docs/Testing.md) - [Filesystem and Processes (lyngio)](docs/lyngio.md) - [Return Statement](docs/return_statement.md) @@ -53,7 +64,7 @@ fun swapEnds(first, args..., last, f) { ```kotlin // update to current please: -val lyngVersion = "0.6.1-SNAPSHOT" +val lyngVersion = "1.5.0-SNAPSHOT" repositories { // ... @@ -166,7 +177,7 @@ Designed to add scripting to kotlin multiplatform application in easy and effici # Language Roadmap -We are now at **v1.0**: basic optimization performed, battery included: standard library is 90% here, initial +We are now at **v1.5.0-SNAPSHOT** (stable development cycle): basic optimization performed, battery included: standard library is 90% here, initial support in HTML, popular editors, and IDEA; tools to syntax highlight and format code are ready. It was released closed to schedule. Ready features: @@ -206,7 +217,7 @@ Ready features: All of this is documented in the [language site](https://lynglang.com) and locally [docs/language.md](docs/tutorial.md). the current nightly builds published on the site and in the private maven repository. -## plan: towards v1.5 Enhancing +## plan: towards v2.0 Next Generation - [x] site with integrated interpreter to give a try - [x] kotlin part public API good docs, integration focused diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md new file mode 100644 index 0000000..f768eab --- /dev/null +++ b/bytecode_migration_plan.md @@ -0,0 +1,7 @@ +# Bytecode Migration Plan (Archived) + +Status: completed. + +Historical reference: +- `notes/archive/bytecode_migration_plan.md` (full plan) +- `notes/archive/bytecode_migration_plan_completed.md` (summary) diff --git a/docs/BytecodeSpec.md b/docs/BytecodeSpec.md new file mode 100644 index 0000000..62a0c35 --- /dev/null +++ b/docs/BytecodeSpec.md @@ -0,0 +1,280 @@ +# Lyng Bytecode VM Spec v0 (Draft) + +This document describes a register-like (3-address) bytecode for Lyng with +dynamic slot width (8/16/32-bit slot IDs), a slot-tail argument model, and +typed lanes for Obj/Int/Real/Bool. The VM is intended to run as a suspendable +interpreter and fall back to the existing AST execution when needed. + +## 1) Frame & Slot Model + +### Frame metadata +- localCount: number of local slots for this function (fixed at compile time). +- argCount: number of arguments passed at call time. +- scopeSlotNames: optional debug names for scope slots (locals/params), aligned to slot mapping. +- argBase = localCount. + +### Slot layout +slots[0 .. localCount-1] locals +slots[localCount .. localCount+argCount-1] arguments + +### Typed lanes +- slotType[]: UNKNOWN/OBJ/INT/REAL/BOOL +- objSlots[], intSlots[], realSlots[], boolSlots[] +- A slot is a logical index; active lane is selected by slotType. + +### Parameter access +- param i => slot localCount + i +- variadic extra => slot localCount + declaredParamCount + k + +### Debug metadata (optional) +- scopeSlotNames: array sized scopeSlotCount, each entry nullable. +- Intended for disassembly/debug tooling; VM semantics do not depend on it. + +### Constant pool extras +- SlotPlan: map of name -> slot index, used by PUSH_SCOPE to pre-allocate and map loop locals. +- CallArgsPlan: ordered argument specs (name/splat) + tailBlock flag, used when argCount has the plan flag set. + +## 2) Slot ID Width + +Per frame, select: +- 8-bit if localCount + argCount < 256 +- 16-bit if < 65536 +- 32-bit otherwise + +The decoder uses a dedicated loop per width. All slot operands are expanded to +Int internally. + +## 3) CALL Semantics (Model A) + +Instruction: +CALL_DIRECT fnId, argBase, argCount, dst + +Behavior: +- Allocate a callee frame sized localCount + argCount. +- Copy caller slots [argBase .. argBase+argCount-1] into callee slots + [localCount .. localCount+argCount-1]. +- Callee returns via RET slot or RET_VOID. +- Caller stores return value to dst. + +Other calls: +- CALL_VIRTUAL recvSlot, methodId, argBase, argCount, dst +- CALL_FALLBACK stmtId, argBase, argCount, dst +- CALL_SLOT calleeSlot, argBase, argCount, dst + +## 4) Binary Encoding Layout + +All instructions are: + [opcode:U8] [operands...] + +Operand widths: +- slotId: S = 1/2/4 bytes (per frame slot width) +- constId: K = 2 bytes (U16), extend to 4 if needed +- ip: I = 2 bytes (U16) or 4 bytes (U32) per function size +- fnId/methodId/stmtId: F/M/T = 2 bytes (U16) unless extended +- argCount: C = 2 bytes (U16), extend to 4 if needed + +Endianness: little-endian for multi-byte operands. + +Common operand patterns: +- S: one slot +- SS: two slots +- SSS: three slots +- K S: constId + dst slot +- S I: slot + jump target +- I: jump target +- F S C S: fnId, argBase slot, argCount, dst slot + +Arg count flag: +- If high bit of C is set (0x8000), the low 15 bits encode a CallArgsPlan constId. +- When not set, C is the raw positional count and tailBlockMode=false. + +## 5) Opcode Table + +Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass. + +### Data movement +- NOP +- MOVE_OBJ S -> S +- MOVE_INT S -> S +- MOVE_REAL S -> S +- MOVE_BOOL S -> S +- BOX_OBJ S -> S +- CONST_OBJ K -> S +- CONST_INT K -> S +- CONST_REAL K -> S +- CONST_BOOL K -> S +- CONST_NULL -> S + +### Numeric conversions +- INT_TO_REAL S -> S +- REAL_TO_INT S -> S +- BOOL_TO_INT S -> S +- INT_TO_BOOL S -> S + +### Arithmetic: INT +- ADD_INT S, S -> S +- SUB_INT S, S -> S +- MUL_INT S, S -> S +- DIV_INT S, S -> S +- MOD_INT S, S -> S +- NEG_INT S -> S +- INC_INT S +- DEC_INT S + +### Arithmetic: REAL +- ADD_REAL S, S -> S +- SUB_REAL S, S -> S +- MUL_REAL S, S -> S +- DIV_REAL S, S -> S +- NEG_REAL S -> S + +### Arithmetic: OBJ +- ADD_OBJ S, S -> S +- SUB_OBJ S, S -> S +- MUL_OBJ S, S -> S +- DIV_OBJ S, S -> S +- MOD_OBJ S, S -> S + +### Bitwise: INT +- AND_INT S, S -> S +- OR_INT S, S -> S +- XOR_INT S, S -> S +- SHL_INT S, S -> S +- SHR_INT S, S -> S +- USHR_INT S, S -> S +- INV_INT S -> S + +### Comparisons (typed) +- CMP_EQ_INT S, S -> S +- CMP_NEQ_INT S, S -> S +- CMP_LT_INT S, S -> S +- CMP_LTE_INT S, S -> S +- CMP_GT_INT S, S -> S +- CMP_GTE_INT S, S -> S +- CMP_EQ_REAL S, S -> S +- CMP_NEQ_REAL S, S -> S +- CMP_LT_REAL S, S -> S +- CMP_LTE_REAL S, S -> S +- CMP_GT_REAL S, S -> S +- CMP_GTE_REAL S, S -> S +- CMP_EQ_BOOL S, S -> S +- CMP_NEQ_BOOL S, S -> S + +### Mixed numeric comparisons +- CMP_EQ_INT_REAL S, S -> S +- CMP_EQ_REAL_INT S, S -> S +- CMP_LT_INT_REAL S, S -> S +- CMP_LT_REAL_INT S, S -> S +- CMP_LTE_INT_REAL S, S -> S +- CMP_LTE_REAL_INT S, S -> S +- CMP_GT_INT_REAL S, S -> S +- CMP_GT_REAL_INT S, S -> S +- CMP_GTE_INT_REAL S, S -> S +- CMP_GTE_REAL_INT S, S -> S +- CMP_NEQ_INT_REAL S, S -> S +- CMP_NEQ_REAL_INT S, S -> S +- CMP_EQ_OBJ S, S -> S +- CMP_NEQ_OBJ S, S -> S +- CMP_REF_EQ_OBJ S, S -> S +- CMP_REF_NEQ_OBJ S, S -> S +- CMP_LT_OBJ S, S -> S +- CMP_LTE_OBJ S, S -> S +- CMP_GT_OBJ S, S -> S +- CMP_GTE_OBJ S, S -> S + +### Boolean ops +- NOT_BOOL S -> S +- AND_BOOL S, S -> S +- OR_BOOL S, S -> S + +### Control flow +- JMP I +- JMP_IF_TRUE S, I +- JMP_IF_FALSE S, I +- RET S +- RET_VOID +- PUSH_SCOPE K +- POP_SCOPE + +### Scope setup +- PUSH_SCOPE uses const `SlotPlan` (name -> slot index) to create a child scope and apply slot mapping. +- POP_SCOPE restores the parent scope. + +### Calls +- CALL_DIRECT F, S, C, S +- CALL_VIRTUAL S, M, S, C, S +- CALL_FALLBACK T, S, C, S +- CALL_SLOT S, S, C, S + +### Object access (optional, later) +- GET_FIELD S, M -> S +- SET_FIELD S, M, S +- GET_INDEX S, S -> S +- SET_INDEX S, S, S + +### Fallback +- EVAL_FALLBACK T -> S + +## 6) Const Pool Encoding (v0) + +Each const entry is encoded as: + [tag:U8] [payload...] + +Tags: +- 0x00: NULL +- 0x01: BOOL (payload: U8 0/1) +- 0x02: INT (payload: S64, little-endian) +- 0x03: REAL (payload: F64, IEEE-754, little-endian) +- 0x04: STRING (payload: U32 length + UTF-8 bytes) +- 0x05: OBJ_REF (payload: U32 index into external Obj table) + +Notes: +- OBJ_REF is reserved for embedding prebuilt Obj handles if needed. +- Strings use UTF-8; length is bytes, not chars. + +## 7) Function Header (binary container) + +Suggested layout for a bytecode function blob: +- magic: U32 ("LYBC") +- version: U16 (0x0001) +- slotWidth: U8 (1,2,4) +- ipWidth: U8 (2,4) +- constIdWidth: U8 (2,4) +- localCount: U32 +- codeSize: U32 (bytes) +- constCount: U32 +- constPool: [const entries...] +- code: [bytecode...] + +Const pool entries use the encoding described in section 6. + +## 8) Sample Bytecode (illustrative) + +Example Lyng: + val x = 2 + val y = 3 + val z = x + y + +Assume: +- localCount = 3 (x,y,z) +- argCount = 0 +- slot width = 1 byte +- const pool: [INT 2, INT 3] + +Bytecode: + CONST_INT k0 -> s0 + CONST_INT k1 -> s1 + ADD_INT s0, s1 -> s2 + RET_VOID + +Encoded (opcode values symbolic): + [OP_CONST_INT][k0][s0] + [OP_CONST_INT][k1][s1] + [OP_ADD_INT][s0][s1][s2] + [OP_RET_VOID] + +## 9) Notes + +- Mixed-mode is allowed: compiler can emit FALLBACK ops for unsupported nodes. +- The VM must be suspendable; on suspension, store ip + minimal operand state. +- Source mapping uses a separate ip->Pos table, not part of core bytecode. diff --git a/docs/OOP.md b/docs/OOP.md index 4057f84..633aee5 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -113,6 +113,48 @@ val handler = object { - **Serialization**: Anonymous objects are **not serializable**. Attempting to encode an anonymous object via `Lynon` will throw a `SerializationException`. This is because their class definition is transient and cannot be safely restored in a different session or process. - **Type Identity**: Every object expression creates a unique anonymous class. Two identical object expressions will result in two different classes with distinct type identities. +## Nested Declarations + +Lyng allows classes, objects, enums, and type aliases to be declared inside another class. These declarations live in the **class namespace** (not the instance), so they do not capture an outer instance and are accessed with a qualifier. + +```lyng +class A { + class B(x?) + object Inner { val foo = "bar" } + type Alias = B + enum E { One, Two } +} + +val ab = A.B() +assertEquals(ab.x, null) +assertEquals(A.Inner.foo, "bar") +``` + +Rules: +- **Qualified access**: use `Outer.Inner` for nested classes/objects/enums/aliases. Inside `Outer` you can refer to them by unqualified name unless shadowed. +- **No inner semantics**: nested declarations do not capture an instance of the outer class. They are resolved at compile time. +- **Visibility**: `private` restricts a nested declaration to the declaring class body (not visible from outside or subclasses). +- **Reflection name**: a nested class reports `Outer.Inner` (e.g., `A.B::class.name` is `"A.B"`). +- **Type aliases**: behave as aliases of the qualified nested type and are expanded by the type system. + +### Lifted Enum Entries + +Enums can optionally lift their entries into the surrounding class namespace using `*`: + +```lyng +class A { + enum E* { One, Two } +} + +assertEquals(A.One, A.E.One) +assertEquals(A.Two, A.E.Two) +``` + +Notes: +- `E*` exposes entries in `A` as if they were direct members (`A.One`). +- If a name would conflict with an existing class member, compilation fails (no implicit fallback). +- Without `*`, use the normal `A.E.One` form. + ## Properties Properties allow you to define member accessors that look like fields but execute code when read or written. Unlike regular fields, properties in Lyng do **not** have automatic backing fields; they are pure accessors. diff --git a/docs/Range.md b/docs/Range.md index 909d0e3..4ad61bc 100644 --- a/docs/Range.md +++ b/docs/Range.md @@ -45,10 +45,11 @@ are equal or within another, taking into account the end-inclusiveness: assert( (1..<3) in (1..3) ) >>> void -## Finite Ranges are iterable +## Ranges are iterable -So given a range with both ends, you can assume it is [Iterable]. This automatically let -use finite ranges in loops and convert it to lists: +Finite ranges are [Iterable] and can be used in loops and list conversions. +Open-ended ranges are iterable only with an explicit `step`, and open-start +ranges are never iterable. assert( [-2, -1, 0, 1] == (-2..1).toList() ) >>> void @@ -62,6 +63,8 @@ In spite of this you can use ranges in for loops: >>> 3 >>> void +The loop variable is read-only inside the loop body (behaves like a `val`). + but for( i in 1..<3 ) @@ -70,6 +73,26 @@ but >>> 2 >>> void +### Stepped ranges + +Use `step` to change the iteration increment. The range bounds still define membership, +so iteration ends when the next value is no longer in the range. + + assert( [1,3,5] == (1..5 step 2).toList() ) + assert( [1,3] == (1..<5 step 2).toList() ) + assert( ['a','c','e'] == ('a'..'e' step 2).toList() ) + >>> void + +Real ranges require an explicit step: + + assert( [0,0.25,0.5,0.75,1.0] == (0.0..1.0 step 0.25).toList() ) + >>> void + +Open-ended ranges require an explicit step to iterate: + + (0.. step 1).take(3).toList() + >>> [0,1,2] + ## Character ranges You can use Char as both ends of the closed range: @@ -98,6 +121,7 @@ Exclusive end char ranges are supported too: | isEndInclusive | true for '..' | Bool | | isOpen | at any end | Bool | | isIntRange | both start and end are Int | Bool | +| step | explicit iteration step | Any? | | start | | Any? | | end | | Any? | | size | for finite ranges, see above | Long | @@ -105,4 +129,4 @@ Exclusive end char ranges are supported too: Ranges are also used with the `clamp(value, range)` function and the `value.clamp(range)` extension method to limit values within boundaries. -[Iterable]: Iterable.md \ No newline at end of file +[Iterable]: Iterable.md diff --git a/docs/advanced_topics.md b/docs/advanced_topics.md index 90c8fd3..1bdfe9d 100644 --- a/docs/advanced_topics.md +++ b/docs/advanced_topics.md @@ -105,6 +105,7 @@ arguments list in almost arbitrary ways. For example: var result = "" for( a in args ) result += a } + // loop variables are read-only inside the loop body assertEquals( "4231", diff --git a/docs/embedding.md b/docs/embedding.md index bada46f..59cf657 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -182,6 +182,77 @@ instance.value = 42 println(instance.value) // -> 42 ``` +### 6.5) Preferred: bind Kotlin implementations to declared Lyng classes + +For extensions and libraries, the **preferred** workflow is Lyng‑first: declare the class and its members in Lyng, then bind the Kotlin implementations using the bridge. + +This keeps Lyng semantics (visibility, overrides, type checks) in Lyng, while Kotlin supplies the behavior. + +```lyng +// Lyng side (in a module) +class Counter { + extern var value: Int + extern fun inc(by: Int): Int +} +``` + +Note: members must be marked `extern` so the compiler emits the ABI slots that Kotlin bindings attach to. This applies to functions and properties bound via `addFun` / `addVal` / `addVar`. + +```kotlin +// Kotlin side (binding) +val moduleScope = Script.newScope() // or an existing module scope +moduleScope.eval("class Counter { extern var value: Int; extern fun inc(by: Int): Int }") + +moduleScope.bind("Counter") { + addVar( + name = "value", + get = { _, self -> self.readField(this, "value").value }, + set = { _, self, v -> self.writeField(this, "value", v) } + ) + addFun("inc") { _, self, args -> + val by = args.requiredArg(0).value + val current = self.readField(this, "value").value as ObjInt + val next = ObjInt(current.value + by) + self.writeField(this, "value", next) + next + } +} +``` + +Notes: + +- Binding must happen **before** the first instance is created. +- Use [LyngClassBridge] to bind by name/module, or by an already resolved `ObjClass`. +- Use `ObjInstance.data` / `ObjClass.classData` to attach Kotlin‑side state when needed. + +### 6.6) Preferred: Kotlin reflection bridge for call‑by‑name + +For Kotlin code that needs dynamic access to Lyng variables, functions, or members, use the bridge resolver. +It provides explicit, cached handles and predictable lookup rules. + +```kotlin +val scope = Script.newScope() +scope.eval(""" + val x = 40 + fun add(a, b) = a + b + class Box { var value = 1 } +""") + +val resolver = scope.resolver() + +// Read a top‑level value +val x = resolver.resolveVal("x").get(scope) + +// Call a function by name (cached inside the resolver) +val sum = (resolver as BridgeCallByName).callByName(scope, "add", Arguments(ObjInt(1), ObjInt(2))) + +// Member access +val box = scope.eval("Box()") +val valueHandle = resolver.resolveMemberVar(box, "value") +valueHandle.set(scope, ObjInt(10)) +val value = valueHandle.get(scope) +``` + ### 7) Read variable values back in Kotlin The simplest approach: evaluate an expression that yields the value and convert it. diff --git a/docs/generics.md b/docs/generics.md new file mode 100644 index 0000000..2c933ed --- /dev/null +++ b/docs/generics.md @@ -0,0 +1,128 @@ +# Generics and type expressions + +This document covers generics, bounds, unions/intersections, and the rules for type expressions in Lyng. + +# Generic parameters + +Declare type parameters with `<...>` on functions and classes: + + fun id(x: T): T = x + class Box(val value: T) + +Type arguments are usually inferred at call sites: + + val b = Box(10) // Box + val s = id("ok") // T is String + +# Bounds + +Use `:` to set bounds. Bounds may be unions (`|`) or intersections (`&`): + + fun sum(x: T, y: T) = x + y + class Named(val data: T) + +Bounds are checked at compile time. For union bounds, the argument must fit at least one option. For intersection bounds, it must fit all options. + +# Variance + +Generic types are invariant by default. You can specify declaration-site variance: + + class Source(val value: T) + class Sink { fun accept(x: T) { ... } } + +`out` makes the type covariant (produced), `in` makes it contravariant (consumed). + +# Type aliases + +Type aliases name type expressions (including unions/intersections): + + type Num = Int | Real + type AB = A & B + +Aliases can be generic and can use bounds and defaults: + + type Maybe = T? + type IntList = List + +Aliases expand to their underlying type expressions. They can be used anywhere a type expression is expected. + +# Inference rules + +- Literals set obvious types (`1` is `Int`, `1.0` is `Real`, etc.). +- Empty list literals default to `List` unless constrained by context. +- Non-empty list literals infer element type as a union of element types. +- Map literals infer key and value types; named keys are `String`. + +Examples: + + val a = [1, 2, 3] // List + val b = [1, "two", true] // List + val c: List = [] // List + + val m1 = { "a": 1, "b": 2 } // Map + val m2 = { "a": 1, "b": "x" } // Map + val m3 = { ...m1, "c": true } // Map + +Map spreads carry key/value types when possible. + +Spreads propagate element type when possible: + + val base = [1, 2] + val mix = [...base, 3] // List + +# Type expressions + +Type expressions include simple types, generics, unions, and intersections: + + Int + List + Int | String + Iterable & Comparable + +These type expressions can appear in casts and `is` checks. + +# `is`, `in`, and `==` with type expressions + +There are two categories of `is` checks: + +1) Value checks: `x is T` + - `x` is a value, `T` is a type expression. + - This is a runtime instance check. + +2) Type checks: `T1 is T2` + - both sides are type expressions (class objects or unions/intersections). + - This is a *type-subset* check: every value of `T1` must fit in `T2`. + +Exact type expression equality uses `==` and is structural (union/intersection order does not matter). + +Includes checks use `in` with type expressions: + + A in T + +This means `A` is a subset of `T` (the same relation as `A is T`). + +Examples (T = A | B): + + T == A // false + T is A // false + A in T // true + B in T // true + T is A | B // true + +# Practical examples + + fun acceptInts(xs: List) { } + acceptInts([1, 2, 3]) + // acceptInts([1, "a"]) -> compile-time error + + fun f(list: List) { + assert( T is Int | String | Bool ) + assert( !(T is Int) ) + assert( Int in T ) + } + f([1, "two", true]) + +# Notes + +- `T` is reified as a type expression when needed (e.g., union/intersection). When it is a single class, `T` is that class object. +- Type expression checks are compile-time where possible; runtime checks only happen for `is` on values and explicit casts. diff --git a/docs/samples/happy_numbers.lyng.bad b/docs/samples/happy_numbers.lyng.bad index ec71429..3bbf68d 100644 --- a/docs/samples/happy_numbers.lyng.bad +++ b/docs/samples/happy_numbers.lyng.bad @@ -4,15 +4,21 @@ test the Lyng way. It is not meant to be effective. */ -fun naiveCountHappyNumbers() { +fun naiveCountHappyNumbers(): Int { var count = 0 - for( n1 in 0..9 ) - for( n2 in 0..9 ) - for( n3 in 0..9 ) - for( n4 in 0..9 ) - for( n5 in 0..9 ) - for( n6 in 0..9 ) + for( n1 in 0..9 ) { + for( n2 in 0..9 ) { + for( n3 in 0..9 ) { + for( n4 in 0..9 ) { + for( n5 in 0..9 ) { + for( n6 in 0..9 ) { if( n1 + n2 + n3 == n4 + n5 + n6 ) count++ + } + } + } + } + } + } count } @@ -28,4 +34,3 @@ for( r in 1..900 ) { assert( found == 55252 ) delay(0.05) } - diff --git a/docs/scopes_and_closures.md b/docs/scopes_and_closures.md index 5e8b0f2..173ba1a 100644 --- a/docs/scopes_and_closures.md +++ b/docs/scopes_and_closures.md @@ -1,5 +1,7 @@ # Scopes and Closures: resolution and safety +Attention to AI: name lookup is ibsolete and must not be used with bytecode compiler + This page documents how name resolution works with `ClosureScope`, how to avoid recursion pitfalls, and how to safely capture and execute callbacks that need access to outer locals. ## Why this matters diff --git a/docs/tutorial.md b/docs/tutorial.md index 0054e34..5162813 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -10,6 +10,7 @@ __Other documents to read__ maybe after this one: - [OOP notes](OOP.md), [exception handling](exceptions_handling.md) - [math in Lyng](math.md), [the `when` statement](when.md), [return statement](return_statement.md) - [Testing and Assertions](Testing.md) +- [Generics and type expressions](generics.md) - [time](time.md) and [parallelism](parallelism.md) - [parallelism] - multithreaded code, coroutines, etc. - Some class @@ -106,6 +107,23 @@ Singleton objects are declared using the `object` keyword. They define a class a Logger.log("Hello singleton!") +## Nested Declarations (short) + +Classes, objects, and enums can be declared inside another class. They live in the class namespace (no outer instance capture), so you access them with a qualifier: + + class A { + class B(x?) + object Inner { val foo = "bar" } + enum E* { One, Two } + } + + val ab = A.B() + assertEquals(ab.x, null) + assertEquals(A.Inner.foo, "bar") + assertEquals(A.One, A.E.One) + +See [OOP notes](OOP.md#nested-declarations) for rules, visibility, and enum lifting details. + ## Delegation (briefly) You can delegate properties and functions to other objects using the `by` keyword. This is perfect for patterns like `lazy` initialization. @@ -224,6 +242,9 @@ This also prevents chain assignments so use parentheses: ## Nullability +Nullability is part of the type. `String` is non-null, `String?` is nullable. Use `!!` to assert non-null and throw +`NullReferenceException` if the value is `null`. + When the value is `null`, it might throws `NullReferenceException`, the name is somewhat a tradition. To avoid it one can check it against null or use _null coalescing_. The null coalescing means, if the operand (left) is null, the operation won't be performed and the result will be null. Here is the difference: @@ -242,6 +263,9 @@ the operation won't be performed and the result will be null. Here is the differ assert( ref?() == null ) >>> void +Note: `?.` is still a typed operation. The receiver must have a compile-time type that declares the member; if the +receiver is `Object`, cast it first or declare a more specific type. + There is also "elvis operator", null-coalesce infix operator '?:' that returns rvalue if lvalue is `null`: null ?: "nothing" @@ -425,8 +449,6 @@ Almost the same, using `val`: val foo = 1 foo += 1 // this will throw exception -# Constants - Same as in kotlin: val HalfPi = π / 2 @@ -434,6 +456,144 @@ Same as in kotlin: Note using greek characters in identifiers! All letters allowed, but remember who might try to read your script, most likely will know some English, the rest is the pure uncertainty. +# Types and inference + +Lyng uses Kotlin-style static types with inference. You can always write explicit types, but in most places the compiler +can infer them from literals, defaults, and flow analysis. + +## Type annotations + +Use `:` to specify a type: + + var x: Int = 10 + val label: String = "count" + fun clamp(x: Int, min: Int, max: Int): Int { ... } + +`Object` is the top type. If you omit a type and there is no default value, the parameter is `Object` by default: + + fun show(x) { println(x) } // x is Object + +For nullable types, add `?`: + + fun showMaybe(x: Object?) { ... } + fun parseInt(s: String?): Int? { ... } + +There is also a nullable shorthand for untyped parameters and constructor args: `x?` means `x: Object?`. +It cannot be combined with an explicit type annotation. + + class A(x?) { ... } // x: Object? + fun f(x?) { x == null } // x: Object? + +Type aliases name type expressions and can be generic: + + type Num = Int | Real + type Maybe = T? + +Aliases expand to their underlying type expressions. See `docs/generics.md` for details. + +`void` is a singleton value of the class `Void`. `Void` can be used as an explicit return type: + + fun log(msg): Void { println(msg); void } + +`Null` is the class of `null`. It is a singleton type and mostly useful for type inference results. + +## Type inference + +The compiler infers types from: + +- literals: `1` is `Int`, `1.0` is `Real`, `"s"` is `String`, `'c'` is `Char` +- defaults: `fun f(x=1, name="n")` infers `x: Int`, `name: String` +- assignments: `val x = call()` uses the return type of `call` +- returns and branches: the result type of a block is the last expression, and if any branch is nullable, + the inferred type becomes nullable +- numeric ops: `Int` and `Real` stay `Int` when both sides are `Int`, and promote to `Real` on mixed arithmetic + +Examples: + + fun inc(x=0) = x + 1 // (Int)->Int + fun maybe(flag) { if(flag) 1 else null } // ()->Int? + +Untyped locals are allowed, but their type is fixed on the first assignment: + + var x + x = 1 // x becomes Int + x = "one" // compile-time error + + var y = null // y is Object? + val z = null // z is Null + +Empty list/map literals default to `List` and `Map` until a more specific type is known: + + val xs = [] // List + val ys: List = [] // List + +Map literals infer key/value types from entries; named keys are `String`. See `docs/generics.md` for details. + +## Flow analysis + +Lyng uses flow analysis to narrow types inside branches: + + fun len(x: String?): Int { + if( x == null ) return 0 + // x is String (non-null) in this branch + return x.length + } + +`is` checks and `when` branches also narrow types: + + fun kind(x: Object) { + if( x is Int ) return "int" + if( x is String ) return "string" + return "other" + } + +Narrowing is local to the branch; after the branch, the original type is restored. + +## Casts and unknown types + +Use `as` for explicit casts. The compiler inserts casts only when it can be valid and necessary. If a cast fails at +runtime, it throws `ClassCastException`. If the value is nullable, `as T` implies a non-null assertion. + +Member access is resolved at compile time. Only members of `Object` are available on unknown types; non-Object members +require an explicit cast: + + fun f(x) { // x is Object + x.toString() // ok (Object member) + x.size() // compile-time error + (x as List).size() // ok + } + +This avoids runtime name-resolution fallbacks; all symbols must be known at compile time. + +## Generics and bounds + +Generic parameters are declared with `<...>`: + + fun id(x: T): T = x + class Box(val value: T) + +Bounds use `:` and can combine with `&` (intersection) and `|` (union): + + fun sum(x: T, y: T) = x + y + class Named(val data: T) + +Type arguments are usually inferred from call sites: + + val b = Box(10) // Box + val s = id("ok") // T is String + +See [Generics and type expressions](generics.md) for bounds, unions/intersections, and type-checking rules. + +## Variance + +Generic types are invariant by default, so `List` is not a `List`. +Use declaration-site variance when you need it: + + class Source(val value: T) + class Sink { fun accept(x: T) { ... } } + +`out` makes the type covariant (only produced), `in` makes it contravariant (only consumed). + # Defining functions fun check(amount) { @@ -579,6 +739,7 @@ See also: [Testing and Assertions](Testing.md) var result = [] for( x in iterable ) result += transform(x) } + // loop variables are read-only inside the loop body assert( [11, 21, 31] == mapValues( [1,2,3], { it*10+1 })) >>> void @@ -1330,7 +1491,7 @@ than enum arrays, until `Lynon.encodeTyped` will be implemented. var result = null // here we will store the result >>> null -# Integral data types +# Built-in types | type | description | literal samples | |--------|---------------------------------|---------------------| @@ -1340,6 +1501,7 @@ than enum arrays, until `Lynon.encodeTyped` will be implemented. | Char | single unicode character | `'S'`, `'\n'` | | String | unicode string, no limits | "hello" (see below) | | List | mutable list | [1, "two", 3] | +| Object | top type for all values | | | Void | no value could exist, singleton | void | | Null | missing value, singleton | null | | Fn | callable type | | @@ -1731,4 +1893,4 @@ Example with custom accessors: Extension members are **scope-isolated**: they are visible only in the scope where they are defined and its children. This prevents name collisions and improves security. -To get details on OOP in Lyng, see [OOP notes](OOP.md). \ No newline at end of file +To get details on OOP in Lyng, see [OOP notes](OOP.md). diff --git a/docs/whats_new.md b/docs/whats_new.md index 59bcddb..17d3f81 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -1,6 +1,7 @@ # What's New in Lyng This document highlights the latest additions and improvements to the Lyng language and its ecosystem. +For a programmer-focused migration summary, see `docs/whats_new_1_5.md`. ## Language Features @@ -101,13 +102,31 @@ Singleton objects are declared using the `object` keyword. They provide a conven ```lyng object Config { - val version = "1.2.3" + val version = "1.5.0-SNAPSHOT" fun show() = println("Config version: " + version) } Config.show() ``` +### Nested Declarations and Lifted Enums +You can now declare classes, objects, enums, and type aliases inside another class. These nested declarations live in the class namespace (no outer instance capture) and are accessed with a qualifier. + +```lyng +class A { + class B(x?) + object Inner { val foo = "bar" } + enum E* { One, Two } +} + +val ab = A.B() +assertEquals(ab.x, null) +assertEquals(A.Inner.foo, "bar") +assertEquals(A.One, A.E.One) +``` + +The `*` on `enum E*` lifts entries into the enclosing class namespace (compile-time error on ambiguity). + ### Object Expressions You can now create anonymous objects that inherit from classes or interfaces using the `object : Base { ... }` syntax. These expressions capture their lexical scope and support multiple inheritance. @@ -224,3 +243,11 @@ You can enable it in **Settings | Lyng Formatter | Enable Lyng autocompletion**. ### Kotlin API: Exception Handling The `Obj.getLyngExceptionMessageWithStackTrace()` extension method has been added to simplify retrieving detailed error information from Lyng exception objects in Kotlin. Additionally, `getLyngExceptionMessage()` and `raiseAsExecutionError()` now accept an optional `Scope`, making it easier to use them when a scope is not immediately available. + +### Kotlin API: Bridge Reflection and Class Binding (Preferred Extensions) +Lyng now provides a public Kotlin reflection bridge and a Lyng‑first class binding workflow. This is the **preferred** way to write Kotlin extensions and library integrations: + +- **Bridge resolver**: explicit handles for values, vars, and callables with predictable lookup rules. +- **Class bridge binding**: declare classes/members in Lyng (marked `extern`) and bind the implementations in Kotlin before the first instance is created. + +See **Embedding Lyng** for full samples and usage details. diff --git a/docs/whats_new_1_3.md b/docs/whats_new_1_3.md new file mode 100644 index 0000000..184a530 --- /dev/null +++ b/docs/whats_new_1_3.md @@ -0,0 +1,133 @@ +# What's New in Lyng 1.3 (vs 1.2.* / master) + +This is a programmer-focused summary of what changed since the 1.2.* line on `master`. It highlights new language and type-system features, runtime/IDE improvements, and how to migrate code safely. + +## Highlights + +- Generics are now a first-class part of the type system, with bounds, variance, unions, and intersections. +- Type aliases and type-expression checks (`T1 is T2`, `A in T`) enable richer static modeling. +- Nested declarations inside classes, plus lifted enum entries via `enum E*`. +- Stepped ranges (`step`) including iterable open-ended and real ranges. +- Runtime and compiler speedups: more bytecode coverage, direct slot access, call-site caching. + +## Language and type system + +### Generics, bounds, and variance + +You can declare generic functions/classes with `<...>`, restrict them with bounds, and control variance. + +```lyng +fun id(x: T): T = x +class Box(val value: T) + +fun sum(x: T, y: T) = x + y +class Named(val data: T) +``` + +### Type aliases and type expressions + +Type aliases can name any type expression, including unions and intersections. + +```lyng +type Num = Int | Real +type Maybe = T? +``` + +Type expressions can be checked directly: + +```lyng +fun f(xs: List) { + assert( T is Int | String | Bool ) // type-subset check + assert( Int in T ) // same relation as `Int is T` +} +``` + +Value checks remain `x is T`. Type expression equality uses `==` and is structural. + +### Nullable shorthand for parameters + +Untyped parameters and constructor args can use `x?` as shorthand for `x: Object?`: + +```lyng +class A(x?) { ... } +fun f(x?) { x == null } +``` + +### List/map literal inference + +The compiler now infers element and key/value types from literals and spreads. Mixed element types produce unions. + +```lyng +val a = [1, 2, 3] // List +val b = [1, "two", true] // List +val m = { "a": 1, "b": "x" } // Map +``` + +### Compile-time member access only + +Member access is resolved at compile time. On unknown types, only `Object` members are visible; other members require an explicit cast. + +```lyng +fun f(x) { // x: Object + x.toString() // ok + x.size() // compile-time error + (x as List).size() +} +``` + +This removes runtime name-resolution fallbacks and makes errors deterministic. + +### Nested declarations and lifted enums + +Classes, objects, enums, and type aliases can be declared inside another class and accessed by qualifier. Enums can lift entries into the outer namespace with `*`. + +```lyng +class A { + class B(x?) + object Inner { val foo = "bar" } + type Alias = B + enum E* { One, Two } +} + +val b = A.B() +assertEquals(A.One, A.E.One) +``` + +### Stepped ranges + +Ranges now support `step`, and open-ended/real ranges are iterable only with an explicit step. + +```lyng +(1..5 step 2).toList() // [1,3,5] +(0.0..1.0 step 0.25).toList() // [0,0.25,0.5,0.75,1.0] +(0.. step 1).take(3).toList() // [0,1,2] +``` + +## Tooling and performance + +- Bytecode compiler/VM coverage expanded (loops, expressions, calls), improving execution speed and consistency. +- Direct frame-slot access and scoped slot addressing reduce lookup overhead, including in closures. +- Call-site caching and numeric fast paths reduce hot-loop overhead. +- IDE tooling updated for the new type system and nested declarations; MiniAst-based completion work continues. + +## Migration guide (from 1.2.*) + +1) Replace dynamic member access on unknown types +- If you relied on runtime name resolution, add explicit casts or annotate types so the compiler can resolve members. + +2) Adopt new type-system constructs where helpful +- Consider `type` aliases for complex unions/intersections. +- Prefer generic signatures over `Object` when the API is parametric. + +3) Update range iteration where needed +- Use `step` for open-ended or real ranges you want to iterate. + +4) Nullable shorthand is optional +- If you used untyped nullable params, you can keep `x` (Object) or switch to `x?` (Object?) for clarity. + +## References + +- `docs/generics.md` +- `docs/Range.md` +- `docs/OOP.md` +- `docs/BytecodeSpec.md` diff --git a/docs/whats_new_1_5.md b/docs/whats_new_1_5.md new file mode 100644 index 0000000..4edfdf7 --- /dev/null +++ b/docs/whats_new_1_5.md @@ -0,0 +1,140 @@ +# What's New in Lyng 1.5 (vs 1.3.* / master) + +This document summarizes the significant changes and new features introduced in the 1.5 development cycle. + +## Principal changes + +### JIT compiler and compile-time types and symbols. + +This major improvement gives the following big advantages: + +- **Blazing Fast execution**: several times faster! (three to six times speedup in different scenarios). +- **Better IDE support**: autocompletion, early error detection, types check. +- **Error safety**: all symbols and types are checked at bound at compile-time. Many errors are detected earlier. Also, no risk that external or caller code would shadow some internally used symbols (especially in closures and inheritance). + +In particular, it means no slow and flaky runtime lookups. Once compiled, code guarantees that it will always call the symbol known at compile-time; runtime name lookup though does not guarantee it and can be source of hard to trace bugs. + +### New stable API to create Kotlin extensions + +The API is fixed and will be kept with further Lyng core changes. It is now the recommended way to write Lyng extensions in Kotlin. It is much simpler and more elegant than the internal one. See [Kotlin Bridge Binding](../notes/kotlin_bridge_binding.md). + +### Smart types system + +- **Deep inference**: The compiler analyzes types of symbols along the execution path and in many cases eliminates unnecessary casts or type specifications. +- **Union and intersection types**: `A & B`, `A | B`. +- **Generics**: Generic types are first-class citizens with support for [bounds and variance](generics.md). No type erasure: in a generic function you can, for example, check `A in T`, where T is the generic type. +- **Inner classes and enums**: Full support for nested declarations, including [Enums with lifting](OOP.md#lifted-enum-entries). + +## Other highlights + +- **The `return` Statement**: Added support for local and non-local returns using labels. +- **Abstract Classes and Interfaces**: Full support for `abstract` members and the `interface` keyword. +- **Singleton Objects**: Define singletons using the `object` keyword or use anonymous object expressions. +- **Multiple Inheritance**: Enhanced multi-inheritance with predictable [C3 MRO resolution](OOP.md#multiple-inheritance-and-mro). +- **Unified Delegation**: Powerful delegation model for `val`, `var`, and `fun` members. See [Delegation](delegation.md). +- **Class Properties with Accessors**: Define `val` and `var` properties with custom `get()` and `set()`. +- **Restricted Setter Visibility**: Use `private set` and `protected set` on fields and properties. +- **Late-initialized `val`**: Support for `val` fields that are initialized in `init` blocks or class bodies. +- **Transient Members**: Use `@Transient` to exclude members from serialization and equality checks. +- **Named Arguments and Splats**: Improved call-site readability with `name: value` and map-based splats. +- **Refined Visibility**: Improved `protected` access and `closed` modifier for better encapsulation. + +## Language Features + +### The `return` Statement +You can now exit from the innermost enclosing callable (function or lambda) using `return`. Lyng also supports non-local returns to outer scopes using labels. See [Return Statement](return_statement.md). + +```lyng +fun findFirst(list: Iterable, predicate: (T)->Bool): T? { + list.forEach { + if (predicate(it)) return@findFirst it + } + null +} +``` + +### Abstract Classes and Interfaces +Lyng now supports the `abstract` modifier for classes and their members. `interface` is introduced as a synonym for `abstract class`, allowing for rich multi-inheritance patterns. + +```lyng +interface Shape { + abstract val area: Real + fun describe() = "Area: %g"(area) +} + +class Circle(val radius: Real) : Shape { + override val area get = Math.PI * radius * radius +} +``` + +### Class Properties with Accessors +Properties can now have custom getters and setters. They do not have automatic backing fields, making them perfect for computed values or delegation. + +```lyng +class Rectangle(var width: Real, var height: Real) { + val area: Real get() = width * height + + var squareSize: Real + get() = area + set(v) { + width = sqrt(v) + height = width + } +} +``` + +### Singleton Objects +Declare singletons or anonymous objects easily. + +```lyng +object Database { + val connection = "connected" +} + +val runner = object : Runnable { + override fun run() = println("Running!") +} +``` + +### Named Arguments and Named Splats +Improve call-site clarity by specifying argument names. You can also expand a Map into named arguments using the splat operator. + +```lyng +fun configure(timeout: Int, retry: Int = 3) { ... } + +configure(timeout: 5000, retry: 5) +val options = Map("timeout": 1000, "retry": 1) +configure(...options) +``` + +### Modern Operators +The `?=` operator allows for concise "assign if null" logic. + +```lyng +var cache: Map? = null +cache ?= Map("status": "ok") // Only assigns if cache is null +``` + +## Tooling and IDE + +- **IDEA Plugin**: Significant improvements to autocompletion, documentation tooltips, and natural language support (Grazie integration). +- **CLI**: The `lyng fmt` command is now a first-class tool for formatting code with various options like `--check` and `--in-place`. +- **Performance**: Ongoing optimizations in the bytecode VM and compiler for faster execution and smaller footprint. + +## Standard Library +- **`with(self, block)`**: Scoped execution with a dedicated `self`. +- **`clamp(value, min, max)`**: Easily restrict values to a range. + +## Migration Guide (from 1.3.*) + +1. **Check Visibility**: Refined `protected` and `private` rules may catch previously undetected invalid accesses. +2. **Override Keyword**: Ensure all members that override ancestor declarations are marked with the `override` keyword (now mandatory). +3. **Return in Shorthand**: Remember that `return` is forbidden in `=` shorthand functions; use block syntax if you need early exit. +4. **Empty Map Literals**: Use `Map()` or `{:}` for empty maps, as `{}` is now strictly a block/lambda. + +## References +- [Object Oriented Programming](OOP.md) +- [Generics](generics.md) +- [Return Statement](return_statement.md) +- [Delegation](delegation.md) +- [Tutorial](tutorial.md) diff --git a/gradle.properties b/gradle.properties index 235d2dd..3fb3995 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # -# Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com +# 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. @@ -16,7 +16,7 @@ # #Gradle -org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" +org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" org.gradle.caching=true org.gradle.configuration-cache=true #Kotlin diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/actions/RunLyngScriptAction.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/actions/RunLyngScriptAction.kt index 1104024..139c519 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/actions/RunLyngScriptAction.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/actions/RunLyngScriptAction.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.launch import net.sergeych.lyng.ExecutionError import net.sergeych.lyng.Script import net.sergeych.lyng.Source +import net.sergeych.lyng.requireScope import net.sergeych.lyng.idea.LyngIcons import net.sergeych.lyng.obj.ObjVoid import net.sergeych.lyng.obj.getLyngExceptionMessageWithStackTrace @@ -81,7 +82,7 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) { val sb = StringBuilder() for ((i, arg) in args.list.withIndex()) { if (i > 0) sb.append(" ") - sb.append(arg.toString(this).value) + sb.append(arg.toString(requireScope()).value) } console.print(sb.toString(), ConsoleViewContentType.NORMAL_OUTPUT) ObjVoid @@ -90,7 +91,7 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) { val sb = StringBuilder() for ((i, arg) in args.list.withIndex()) { if (i > 0) sb.append(" ") - sb.append(arg.toString(this).value) + sb.append(arg.toString(requireScope()).value) } console.print(sb.toString() + "\n", ConsoleViewContentType.NORMAL_OUTPUT) ObjVoid diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt index bb2a1ab..c8288ae 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt @@ -25,15 +25,13 @@ import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.util.Key import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import net.sergeych.lyng.Source -import net.sergeych.lyng.binding.Binder -import net.sergeych.lyng.binding.SymbolKind import net.sergeych.lyng.highlight.HighlightKind -import net.sergeych.lyng.highlight.SimpleLyngHighlighter import net.sergeych.lyng.highlight.offsetOf import net.sergeych.lyng.idea.highlight.LyngHighlighterColors import net.sergeych.lyng.idea.util.LyngAstManager -import net.sergeych.lyng.miniast.* +import net.sergeych.lyng.tools.LyngDiagnosticSeverity +import net.sergeych.lyng.tools.LyngLanguageTools +import net.sergeych.lyng.tools.LyngSemanticKind /** * ExternalAnnotator that runs Lyng MiniAst on the document text in background @@ -43,8 +41,8 @@ class LyngExternalAnnotator : ExternalAnnotator?, val file: PsiFile) data class Span(val start: Int, val end: Int, val key: com.intellij.openapi.editor.colors.TextAttributesKey) - data class Error(val start: Int, val end: Int, val message: String) - data class Result(val modStamp: Long, val spans: List, val error: Error? = null) + data class Diag(val start: Int, val end: Int, val message: String, val severity: HighlightSeverity) + data class Result(val modStamp: Long, val spans: List, val diagnostics: List = emptyList()) override fun collectInformation(file: PsiFile): Input? { val doc: Document = file.viewProvider.document ?: return null @@ -59,224 +57,46 @@ class LyngExternalAnnotator : ExternalAnnotator(256) + val mini = analysis.mini - fun isFollowedByParenOrBlock(rangeEnd: Int): Boolean { - var i = rangeEnd - while (i < text.length) { - val ch = text[i] - if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') { i++; continue } - return ch == '(' || ch == '{' - } - return false - } + ProgressManager.checkCanceled() + + val out = ArrayList(256) + val diags = ArrayList() fun putRange(start: Int, end: Int, key: com.intellij.openapi.editor.colors.TextAttributesKey) { if (start in 0..end && end <= text.length && start < end) out += Span(start, end, key) } - fun putName(startPos: net.sergeych.lyng.Pos, name: String, key: com.intellij.openapi.editor.colors.TextAttributesKey) { - val s = source.offsetOf(startPos) - putRange(s, (s + name.length).coerceAtMost(text.length), key) - } - fun putMiniRange(r: MiniRange, key: com.intellij.openapi.editor.colors.TextAttributesKey) { - val s = source.offsetOf(r.start) - val e = source.offsetOf(r.end) - putRange(s, e, key) + + fun keyForKind(kind: LyngSemanticKind): com.intellij.openapi.editor.colors.TextAttributesKey? = when (kind) { + LyngSemanticKind.Function -> LyngHighlighterColors.FUNCTION + LyngSemanticKind.Class, LyngSemanticKind.Enum, LyngSemanticKind.TypeAlias -> LyngHighlighterColors.TYPE + LyngSemanticKind.Value -> LyngHighlighterColors.VALUE + LyngSemanticKind.Variable -> LyngHighlighterColors.VARIABLE + LyngSemanticKind.Parameter -> LyngHighlighterColors.PARAMETER + LyngSemanticKind.TypeRef -> LyngHighlighterColors.TYPE + LyngSemanticKind.EnumConstant -> LyngHighlighterColors.ENUM_CONSTANT } - // Declarations - mini.declarations.forEach { d -> - if (d.nameStart.source != source) return@forEach - when (d) { - is MiniFunDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.FUNCTION_DECLARATION) - is MiniClassDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE) - is MiniValDecl -> putName( - d.nameStart, - d.name, - if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE - ) - is MiniEnumDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE) - } + // Semantic highlights from shared tooling + LyngLanguageTools.semanticHighlights(analysis).forEach { span -> + keyForKind(span.kind)?.let { putRange(span.range.start, span.range.endExclusive, it) } } // Imports: each segment as namespace/path - mini.imports.forEach { imp -> - if (imp.range.start.source != source) return@forEach - imp.segments.forEach { seg -> putMiniRange(seg.range, LyngHighlighterColors.NAMESPACE) } - } - - // Parameters - fun addParams(params: List) { - params.forEach { p -> - if (p.nameStart.source == source) - putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER) + mini?.imports?.forEach { imp -> + imp.segments.forEach { seg -> + val start = analysis.source.offsetOf(seg.range.start) + val end = analysis.source.offsetOf(seg.range.end) + putRange(start, end, LyngHighlighterColors.NAMESPACE) } } - mini.declarations.forEach { d -> - when (d) { - is MiniFunDecl -> addParams(d.params) - is MiniClassDecl -> d.members.filterIsInstance().forEach { addParams(it.params) } - else -> {} - } - } - - // Type name segments (including generics base & args) - fun addTypeSegments(t: MiniTypeRef?) { - when (t) { - is MiniTypeName -> t.segments.forEach { seg -> - if (seg.range.start.source != source) return@forEach - val s = source.offsetOf(seg.range.start) - putRange(s, (s + seg.name.length).coerceAtMost(text.length), LyngHighlighterColors.TYPE) - } - is MiniGenericType -> { - addTypeSegments(t.base) - t.args.forEach { addTypeSegments(it) } - } - is MiniFunctionType -> { - t.receiver?.let { addTypeSegments(it) } - t.params.forEach { addTypeSegments(it) } - addTypeSegments(t.returnType) - } - is MiniTypeVar -> { /* name is in range; could be highlighted as TYPE as well */ - if (t.range.start.source == source) - putMiniRange(t.range, LyngHighlighterColors.TYPE) - } - null -> {} - } - } - fun addDeclTypeSegments(d: MiniDecl) { - if (d.nameStart.source != source) return - when (d) { - is MiniFunDecl -> { - addTypeSegments(d.returnType) - d.params.forEach { addTypeSegments(it.type) } - addTypeSegments(d.receiver) - } - is MiniValDecl -> { - addTypeSegments(d.type) - addTypeSegments(d.receiver) - } - is MiniClassDecl -> { - d.ctorFields.forEach { addTypeSegments(it.type) } - d.classFields.forEach { addTypeSegments(it.type) } - for (m in d.members) { - when (m) { - is MiniMemberFunDecl -> { - addTypeSegments(m.returnType) - m.params.forEach { addTypeSegments(it.type) } - } - is MiniMemberValDecl -> { - addTypeSegments(m.type) - } - else -> {} - } - } - } - is MiniEnumDecl -> {} - } - } - mini.declarations.forEach { d -> addDeclTypeSegments(d) } - - ProgressManager.checkCanceled() - - // Semantic usages via Binder (best-effort) - try { - val binding = Binder.bind(text, mini) - - // Map declaration ranges to avoid duplicating them as usages - val declKeys = HashSet>(binding.symbols.size * 2) - binding.symbols.forEach { sym -> declKeys += (sym.declStart to sym.declEnd) } - - fun keyForKind(k: SymbolKind) = when (k) { - SymbolKind.Function -> LyngHighlighterColors.FUNCTION - SymbolKind.Class, SymbolKind.Enum -> LyngHighlighterColors.TYPE - SymbolKind.Parameter -> LyngHighlighterColors.PARAMETER - SymbolKind.Value -> LyngHighlighterColors.VALUE - SymbolKind.Variable -> LyngHighlighterColors.VARIABLE - } - - // Track covered ranges to not override later heuristics - val covered = HashSet>() - - binding.references.forEach { ref -> - val key = ref.start to ref.end - if (!declKeys.contains(key)) { - val sym = binding.symbols.firstOrNull { it.id == ref.symbolId } - if (sym != null) { - val color = keyForKind(sym.kind) - putRange(ref.start, ref.end, color) - covered += key - } - } - } - - // Heuristics on top of binder: function call-sites and simple name-based roles - ProgressManager.checkCanceled() - - // Build simple name -> role map for top-level vals/vars and parameters - val nameRole = HashMap(8) - mini.declarations.forEach { d -> - when (d) { - is MiniValDecl -> nameRole[d.name] = - if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE - - is MiniFunDecl -> d.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER } - is MiniClassDecl -> { - d.members.forEach { m -> - if (m is MiniMemberFunDecl) { - m.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER } - } - } - } - else -> {} - } - } - - tokens.forEach { s -> - if (s.kind == HighlightKind.Identifier) { - val start = s.range.start - val end = s.range.endExclusive - val key = start to end - if (key !in covered && key !in declKeys) { - // Call-site detection first so it wins over var/param role - if (isFollowedByParenOrBlock(end)) { - putRange(start, end, LyngHighlighterColors.FUNCTION) - covered += key - } else { - // Simple role by known names - val ident = try { - text.substring(start, end) - } catch (_: Throwable) { - null - } - if (ident != null) { - val roleKey = nameRole[ident] - if (roleKey != null) { - putRange(start, end, roleKey) - covered += key - } - } - } - } - } - } - } catch (e: Throwable) { - // Must rethrow cancellation; otherwise ignore binder failures (best-effort) - if (e is com.intellij.openapi.progress.ProcessCanceledException) throw e - } // Add annotation/label coloring using token highlighter run { - tokens.forEach { s -> + analysis.lexicalHighlights.forEach { s -> if (s.kind == HighlightKind.Label) { val start = s.range.start val end = s.range.endExclusive @@ -302,7 +122,7 @@ class LyngExternalAnnotator : ExternalAnnotator - if (s.kind == HighlightKind.EnumConstant) { - val start = s.range.start - val end = s.range.endExclusive - if (start in 0..end && end <= text.length && start < end) { - putRange(start, end, LyngHighlighterColors.ENUM_CONSTANT) - } - } + analysis.diagnostics.forEach { d -> + val range = d.range ?: return@forEach + val severity = if (d.severity == LyngDiagnosticSeverity.Warning) HighlightSeverity.WARNING else HighlightSeverity.ERROR + diags += Diag(range.start, range.endExclusive, d.message, severity) } - return Result(collectedInfo.modStamp, out, null) + return Result(collectedInfo.modStamp, out, diags) } @@ -346,13 +162,12 @@ class LyngExternalAnnotator : ExternalAnnotator + val start = d.start.coerceIn(0, (doc?.textLength ?: 0)) + val end = d.end.coerceIn(start, (doc?.textLength ?: start)) if (end > start) { - holder.newAnnotation(HighlightSeverity.ERROR, err.message) + holder.newAnnotation(d.severity, d.message) .range(TextRange(start, end)) .create() } @@ -373,30 +188,5 @@ class LyngExternalAnnotator : ExternalAnnotator { - if (text.isEmpty()) return 0 to 0 - val len = text.length - val start = rawStart.coerceIn(0, len) - fun isWord(ch: Char) = ch == '_' || ch.isLetterOrDigit() - - if (start < len && isWord(text[start])) { - var s = start - var e = start - while (s > 0 && isWord(text[s - 1])) s-- - while (e < len && isWord(text[e])) e++ - return s to e - } - - // Not inside a word: select a short, visible range up to EOL - val lineEnd = text.indexOf('\n', start).let { if (it == -1) len else it } - val minWidth = 4 - val end = (start + minWidth).coerceAtMost(lineEnd).coerceAtLeast((start + 1).coerceAtMost(lineEnd)) - return start to end - } + } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt index 62925ac..8f8ca60 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt @@ -96,9 +96,10 @@ class LyngCompletionContributor : CompletionContributor() { log.info("[LYNG_DEBUG] Completion: caret=$caret prefix='${prefix}' memberDotPos=${memberDotPos} file='${file.name}'") } - // Build MiniAst (cached) for both global and member contexts to enable local class/val inference - val mini = LyngAstManager.getMiniAst(file) - val binding = LyngAstManager.getBinding(file) + // Build analysis (cached) for both global and member contexts to enable local class/val inference + val analysis = LyngAstManager.getAnalysis(file) + val mini = analysis?.mini + val binding = analysis?.binding // Delegate computation to the shared engine to keep behavior in sync with tests val engineItems = try { @@ -121,6 +122,7 @@ class LyngCompletionContributor : CompletionContributor() { fromText.forEach { add(it) } add("lyng.stdlib") }.toList() + val staticOnly = DocLookupUtils.isStaticReceiver(mini, text, memberDotPos, imported, binding) // Try inferring return/receiver class around the dot val inferred = @@ -135,7 +137,7 @@ class LyngCompletionContributor : CompletionContributor() { if (inferred != null) { if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Fallback inferred receiver/return class='$inferred' — offering its members") - offerMembers(emit, imported, inferred, sourceText = text, mini = mini) + offerMembers(emit, imported, inferred, staticOnly = staticOnly, sourceText = text, mini = mini) return } else { if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Fallback could not infer class; keeping list empty (no globals after dot)") @@ -160,6 +162,8 @@ class LyngCompletionContributor : CompletionContributor() { .withIcon(AllIcons.Nodes.Class) Kind.Enum -> LookupElementBuilder.create(ci.name) .withIcon(AllIcons.Nodes.Enum) + Kind.TypeAlias -> LookupElementBuilder.create(ci.name) + .withIcon(AllIcons.Nodes.Class) Kind.Value -> LookupElementBuilder.create(ci.name) .withIcon(AllIcons.Nodes.Variable) .let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b } @@ -292,6 +296,9 @@ class LyngCompletionContributor : CompletionContributor() { } is MiniEnumDecl -> LookupElementBuilder.create(name) .withIcon(AllIcons.Nodes.Enum) + is MiniTypeAliasDecl -> LookupElementBuilder.create(name) + .withIcon(AllIcons.Nodes.Class) + .withTypeText(typeOf(d.target), true) } emit(builder) } @@ -369,6 +376,7 @@ class LyngCompletionContributor : CompletionContributor() { when (m) { is MiniMemberFunDecl -> if (!m.isStatic) continue is MiniMemberValDecl -> if (!m.isStatic) continue + is MiniMemberTypeAliasDecl -> if (!m.isStatic) continue is MiniInitDecl -> continue } } @@ -458,6 +466,16 @@ class LyngCompletionContributor : CompletionContributor() { emit(builder) } } + is MiniMemberTypeAliasDecl -> { + val builder = LookupElementBuilder.create(name) + .withIcon(AllIcons.Nodes.Class) + .withTypeText(typeOf(rep.target), true) + if (groupPriority != 0.0) { + emit(PrioritizedLookupElement.withPriority(builder, groupPriority)) + } else { + emit(builder) + } + } is MiniInitDecl -> {} } } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt index 82e4cef..03fc1a8 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt @@ -29,6 +29,8 @@ import net.sergeych.lyng.idea.LyngLanguage import net.sergeych.lyng.idea.util.LyngAstManager import net.sergeych.lyng.idea.util.TextCtx import net.sergeych.lyng.miniast.* +import net.sergeych.lyng.tools.LyngLanguageTools +import net.sergeych.lyng.tools.LyngSymbolInfo /** * Quick Docs backed by MiniAst: when caret is on an identifier that corresponds @@ -66,9 +68,15 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: ident='$ident' at ${idRange.startOffset}..${idRange.endOffset} in ${file.name}") // 1. Get merged mini-AST from Manager (handles local + .lyng.d merged declarations) - val mini = LyngAstManager.getMiniAst(file) ?: return null + val analysis = LyngAstManager.getAnalysis(file) ?: return null + val mini = analysis.mini ?: return null val miniSource = mini.range.start.source - val imported = DocLookupUtils.canonicalImportedModules(mini, text) + val imported = analysis.importedModules.ifEmpty { DocLookupUtils.canonicalImportedModules(mini, text) } + + // Single-source quick doc lookup + LyngLanguageTools.docAt(analysis, offset)?.let { info -> + renderDocFromInfo(info)?.let { return it } + } // Try resolve to: function param at position, function/class/val declaration at position // 1) Use unified declaration detection @@ -91,6 +99,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (m) { is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m) is MiniMemberValDecl -> renderMemberValDoc(d.name, m) + is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(d.name, m) else -> null } } @@ -197,6 +206,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (m) { is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m) is MiniMemberValDecl -> renderMemberValDoc(cls.name, m) + is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(cls.name, m) else -> null } } @@ -307,16 +317,19 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { } if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: memberCtx dotPos=${dotPos} chBeforeDot='${if (dotPos > 0) text[dotPos - 1] else ' '}' classGuess=${className} imports=${importedModules}") if (className != null) { - DocLookupUtils.resolveMemberWithInheritance(importedModules, className, ident, mini)?.let { (owner, member) -> + val staticOnly = DocLookupUtils.isStaticReceiver(mini, text, dotPos, importedModules, analysis.binding) + DocLookupUtils.resolveMemberWithInheritance(importedModules, className, ident, mini, staticOnly = staticOnly)?.let { (owner, member) -> if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] QuickDoc: literal/call '$ident' resolved to $owner.${member.name}") return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member) is MiniInitDecl -> null is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules) + is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules) } } log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}") @@ -354,6 +367,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { // And classes/enums docs.filterIsInstance().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) } docs.filterIsInstance().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) } + docs.filterIsInstance().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) } } // Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs if (ident == "println" || ident == "print") { @@ -367,16 +381,20 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { val lhs = previousWordBefore(text, idRange.startOffset) if (lhs != null && hasDotBetween(text, lhs.endOffset, idRange.startOffset)) { val className = text.substring(lhs.startOffset, lhs.endOffset) - DocLookupUtils.resolveMemberWithInheritance(importedModules, className, ident, mini)?.let { (owner, member) -> + val dotPos = findDotLeft(text, idRange.startOffset) + val staticOnly = dotPos?.let { DocLookupUtils.isStaticReceiver(mini, text, it, importedModules, analysis.binding) } ?: false + DocLookupUtils.resolveMemberWithInheritance(importedModules, className, ident, mini, staticOnly = staticOnly)?.let { (owner, member) -> if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Inheritance resolved $className.$ident to $owner.${member.name}") return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member) is MiniInitDecl -> null is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules) + is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules) } } } else { @@ -390,16 +408,19 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { else -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules, mini) } if (guessed != null) { - DocLookupUtils.resolveMemberWithInheritance(importedModules, guessed, ident, mini)?.let { (owner, member) -> + val staticOnly = DocLookupUtils.isStaticReceiver(mini, text, dotPos, importedModules, analysis.binding) + DocLookupUtils.resolveMemberWithInheritance(importedModules, guessed, ident, mini, staticOnly = staticOnly)?.let { (owner, member) -> if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Heuristic '$guessed.$ident' resolved via inheritance to $owner.${member.name}") return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member) is MiniInitDecl -> null is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules) + is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules) } } } else { @@ -407,16 +428,19 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { run { val candidates = listOf("String", "Iterable", "Iterator", "List", "Collection", "Array", "Dict", "Regex") for (c in candidates) { - DocLookupUtils.resolveMemberWithInheritance(importedModules, c, ident, mini)?.let { (owner, member) -> + val staticOnly = DocLookupUtils.isStaticReceiver(mini, text, dotPos, importedModules, analysis.binding) + DocLookupUtils.resolveMemberWithInheritance(importedModules, c, ident, mini, staticOnly = staticOnly)?.let { (owner, member) -> if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Candidate '$c.$ident' resolved via inheritance to $owner.${member.name}") return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member) is MiniInitDecl -> null is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules) + is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules) } } } @@ -431,6 +455,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (m) { is MiniMemberFunDecl -> renderMemberFunDoc("String", m) is MiniMemberValDecl -> renderMemberValDoc("String", m) + is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc("String", m) is MiniInitDecl -> null } } @@ -441,11 +466,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member) is MiniInitDecl -> null is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules) is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules) + is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules) } } } @@ -512,6 +539,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { is MiniFunDecl -> "function ${d.name}${signatureOf(d)}" is MiniClassDecl -> "class ${d.name}" is MiniEnumDecl -> "enum ${d.name} { ${d.entries.joinToString(", ")} }" + is MiniTypeAliasDecl -> "type ${d.name}${typeAliasSuffix(d)}" is MiniValDecl -> { val t = d.type ?: DocLookupUtils.inferTypeRefForVal(d, text, imported, mini) val typeStr = if (t == null) ": Object?" else typeOf(t) @@ -524,6 +552,24 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return sb.toString() } + private fun renderDocFromInfo(info: LyngSymbolInfo): String? { + val kind = when (info.target.kind) { + net.sergeych.lyng.binding.SymbolKind.Function -> "function" + net.sergeych.lyng.binding.SymbolKind.Class -> "class" + net.sergeych.lyng.binding.SymbolKind.Enum -> "enum" + net.sergeych.lyng.binding.SymbolKind.TypeAlias -> "type" + net.sergeych.lyng.binding.SymbolKind.Value -> "val" + net.sergeych.lyng.binding.SymbolKind.Variable -> "var" + net.sergeych.lyng.binding.SymbolKind.Parameter -> "parameter" + } + val title = info.signature ?: "$kind ${info.target.name}" + if (title.isBlank() && info.doc == null) return null + val sb = StringBuilder() + sb.append(renderTitle(title)) + sb.append(renderDocBody(info.doc)) + return sb.toString() + } + private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String { val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}" val sb = StringBuilder() @@ -565,6 +611,25 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return sb.toString() } + private fun renderMemberTypeAliasDoc(className: String, m: MiniMemberTypeAliasDecl): String { + val tp = if (m.typeParams.isEmpty()) "" else "<" + m.typeParams.joinToString(", ") + ">" + val body = typeOf(m.target) + val rhs = if (body.isBlank()) "" else " = ${body.removePrefix(": ")}" + val staticStr = if (m.isStatic) "static " else "" + val title = "${staticStr}type $className.${m.name}$tp$rhs" + val sb = StringBuilder() + sb.append(renderTitle(title)) + sb.append(renderDocBody(m.doc)) + return sb.toString() + } + + private fun typeAliasSuffix(d: MiniTypeAliasDecl): String { + val tp = if (d.typeParams.isEmpty()) "" else "<" + d.typeParams.joinToString(", ") + ">" + val body = typeOf(d.target) + val rhs = if (body.isBlank()) "" else " = ${body.removePrefix(": ")}" + return "$tp$rhs" + } + private fun typeOf(t: MiniTypeRef?): String { val s = DocLookupUtils.typeOf(t) return if (s.isEmpty()) (if (t == null) ": Object?" else "") else ": $s" diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt index b2f02a7..bd14f57 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt @@ -36,7 +36,7 @@ class LyngLexer : LexerBase() { "abstract", "closed", "override", "static", "extern", "open", "private", "protected", "if", "else", "for", "while", "return", "true", "false", "null", "when", "in", "is", "break", "continue", "try", "catch", "finally", - "get", "set", "object", "enum", "init", "by", "property", "constructor" + "get", "set", "object", "enum", "init", "by", "step", "property", "constructor" ) override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) { diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt index 95ea04e..33b872c 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt @@ -36,9 +36,10 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase() - val mini = LyngAstManager.getMiniAst(file) ?: return emptyArray() - val binding = LyngAstManager.getBinding(file) - val imported = DocLookupUtils.canonicalImportedModules(mini, text).toSet() + val analysis = LyngAstManager.getAnalysis(file) ?: return emptyArray() + val mini = analysis.mini ?: return emptyArray() + val binding = analysis.binding + val imported = analysis.importedModules.toSet() val currentPackage = getPackageName(file) val allowedPackages = if (currentPackage != null) imported + currentPackage else imported @@ -47,9 +48,10 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase "Function" is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value" + is MiniMemberTypeAliasDecl -> "TypeAlias" is MiniInitDecl -> "Initializer" is MiniFunDecl -> "Function" is MiniValDecl -> if (member.mutable) "Variable" else "Value" is MiniClassDecl -> "Class" is MiniEnumDecl -> "Enum" + is MiniTypeAliasDecl -> "TypeAlias" } results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind))) } @@ -199,6 +203,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase "Class" is net.sergeych.lyng.miniast.MiniEnumDecl -> "Enum" is net.sergeych.lyng.miniast.MiniValDecl -> if (d.mutable) "Variable" else "Value" + is net.sergeych.lyng.miniast.MiniTypeAliasDecl -> "TypeAlias" } addIfMatch(d.name, d.nameStart, dKind) } @@ -214,6 +219,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase "Function" is net.sergeych.lyng.miniast.MiniMemberValDecl -> if (m.mutable) "Variable" else "Value" + is net.sergeych.lyng.miniast.MiniMemberTypeAliasDecl -> "TypeAlias" is net.sergeych.lyng.miniast.MiniInitDecl -> "Initializer" } addIfMatch(m.name, m.nameStart, mKind) diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt index a93b554..e6fa649 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt @@ -22,55 +22,22 @@ import com.intellij.openapi.util.Key import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import kotlinx.coroutines.runBlocking -import net.sergeych.lyng.Compiler -import net.sergeych.lyng.Source -import net.sergeych.lyng.binding.Binder import net.sergeych.lyng.binding.BindingSnapshot -import net.sergeych.lyng.miniast.MiniAstBuilder +import net.sergeych.lyng.miniast.DocLookupUtils import net.sergeych.lyng.miniast.MiniScript +import net.sergeych.lyng.tools.IdeLenientImportProvider +import net.sergeych.lyng.tools.LyngAnalysisRequest +import net.sergeych.lyng.tools.LyngAnalysisResult +import net.sergeych.lyng.tools.LyngLanguageTools object LyngAstManager { private val MINI_KEY = Key.create("lyng.mini.cache") private val BINDING_KEY = Key.create("lyng.binding.cache") private val STAMP_KEY = Key.create("lyng.mini.cache.stamp") + private val ANALYSIS_KEY = Key.create("lyng.analysis.cache") fun getMiniAst(file: PsiFile): MiniScript? = runReadAction { - val vFile = file.virtualFile ?: return@runReadAction null - val combinedStamp = getCombinedStamp(file) - - val prevStamp = file.getUserData(STAMP_KEY) - val cached = file.getUserData(MINI_KEY) - if (cached != null && prevStamp != null && prevStamp == combinedStamp) return@runReadAction cached - - val text = file.viewProvider.contents.toString() - val sink = MiniAstBuilder() - val built = try { - val provider = IdeLenientImportProvider.create() - val src = Source(file.name, text) - runBlocking { Compiler.compileWithMini(src, provider, sink) } - val script = sink.build() - if (script != null && !file.name.endsWith(".lyng.d")) { - val dFiles = collectDeclarationFiles(file) - for (df in dFiles) { - val scriptD = getMiniAst(df) - if (scriptD != null) { - script.declarations.addAll(scriptD.declarations) - script.imports.addAll(scriptD.imports) - } - } - } - script - } catch (_: Throwable) { - sink.build() - } - - if (built != null) { - file.putUserData(MINI_KEY, built) - file.putUserData(STAMP_KEY, combinedStamp) - // Invalidate binding too - file.putUserData(BINDING_KEY, null) - } - built + getAnalysis(file)?.mini } fun getCombinedStamp(file: PsiFile): Long = runReadAction { @@ -102,32 +69,53 @@ object LyngAstManager { } fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction { + getAnalysis(file)?.binding + } + + fun getAnalysis(file: PsiFile): LyngAnalysisResult? = runReadAction { val vFile = file.virtualFile ?: return@runReadAction null - var combinedStamp = file.viewProvider.modificationStamp - - val dFiles = if (!file.name.endsWith(".lyng.d")) collectDeclarationFiles(file) else emptyList() - for (df in dFiles) { - combinedStamp += df.viewProvider.modificationStamp - } - + val combinedStamp = getCombinedStamp(file) val prevStamp = file.getUserData(STAMP_KEY) - val cached = file.getUserData(BINDING_KEY) - + val cached = file.getUserData(ANALYSIS_KEY) if (cached != null && prevStamp != null && prevStamp == combinedStamp) return@runReadAction cached - val mini = getMiniAst(file) ?: return@runReadAction null val text = file.viewProvider.contents.toString() - val binding = try { - Binder.bind(text, mini) + val built = try { + val provider = IdeLenientImportProvider.create() + runBlocking { + LyngLanguageTools.analyze( + LyngAnalysisRequest(text = text, fileName = file.name, importProvider = provider) + ) + } } catch (_: Throwable) { null } - if (binding != null) { - file.putUserData(BINDING_KEY, binding) - // stamp is already set by getMiniAst or we set it here if getMiniAst was cached + if (built != null) { + val merged = built.mini + if (merged != null && !file.name.endsWith(".lyng.d")) { + val dFiles = collectDeclarationFiles(file) + for (df in dFiles) { + val dAnalysis = getAnalysis(df) + val dMini = dAnalysis?.mini ?: continue + merged.declarations.addAll(dMini.declarations) + merged.imports.addAll(dMini.imports) + } + } + val finalAnalysis = if (merged != null) { + built.copy( + mini = merged, + importedModules = DocLookupUtils.canonicalImportedModules(merged, text) + ) + } else { + built + } + file.putUserData(ANALYSIS_KEY, finalAnalysis) + file.putUserData(MINI_KEY, finalAnalysis.mini) + file.putUserData(BINDING_KEY, finalAnalysis.binding) file.putUserData(STAMP_KEY, combinedStamp) + return@runReadAction finalAnalysis } - binding + null } } diff --git a/lyng/src/commonMain/kotlin/Common.kt b/lyng/src/commonMain/kotlin/Common.kt index 1320f9d..80bed57 100644 --- a/lyng/src/commonMain/kotlin/Common.kt +++ b/lyng/src/commonMain/kotlin/Common.kt @@ -27,6 +27,7 @@ import com.github.ajalt.clikt.parameters.arguments.optional import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.Compiler import net.sergeych.lyng.LyngVersion import net.sergeych.lyng.Script import net.sergeych.lyng.ScriptError @@ -167,7 +168,7 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() override fun help(context: Context): String = """ - The Lyng script language interpreter, language version is $LyngVersion. + The Lyng script language runtime, language version is $LyngVersion. Please refer form more information to the project site: https://gitea.sergeych.net/SergeychWorks/lyng @@ -198,7 +199,12 @@ private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() launcher { // there is no script name, it is a first argument instead: processErrors { - baseScope.eval(execute!!) + val script = Compiler.compileWithResolution( + Source("", execute!!), + baseScope.currentImportProvider, + seedScope = baseScope + ) + script.execute(baseScope) } } } @@ -236,7 +242,13 @@ suspend fun executeFile(fileName: String) { text = text.substring(pos + 1) } processErrors { - baseScopeDefer.await().eval(Source(fileName, text)) + val scope = baseScopeDefer.await() + val script = Compiler.compileWithResolution( + Source(fileName, text), + scope.currentImportProvider, + seedScope = scope + ) + script.execute(scope) } } diff --git a/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/FsIntegrationJvmTest.kt b/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/FsIntegrationJvmTest.kt index ae606d3..2484fa7 100644 --- a/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/FsIntegrationJvmTest.kt +++ b/lyng/src/jvmTest/kotlin/net/sergeych/lyng_cli/FsIntegrationJvmTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * 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. @@ -56,7 +56,7 @@ class FsIntegrationJvmTest { """ import lyng.io.fs // list current folder files - println( Path(".").list().toList() ) + println( Path(".").list() ) """.trimIndent() ) } diff --git a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/fs/LyngFsModule.kt b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/fs/LyngFsModule.kt index 5c30fc2..ecff150 100644 --- a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/fs/LyngFsModule.kt +++ b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/fs/LyngFsModule.kt @@ -23,6 +23,8 @@ package net.sergeych.lyng.io.fs import net.sergeych.lyng.ModuleScope import net.sergeych.lyng.Scope +import net.sergeych.lyng.ScopeFacade +import net.sergeych.lyng.requireScope import net.sergeych.lyng.miniast.* import net.sergeych.lyng.obj.* import net.sergeych.lyng.pacman.ImportManager @@ -437,7 +439,7 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) { moduleName = module.packageName ) { fsGuard { - val chunkIt = thisObj.invokeInstanceMethod(this, "readUtf8Chunks") + val chunkIt = thisObj.invokeInstanceMethod(requireScope(), "readUtf8Chunks") ObjFsLinesIterator(chunkIt) } } @@ -463,7 +465,7 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) { // --- Helper classes and utilities --- -private fun parsePathArg(scope: Scope, self: ObjPath, arg: Obj): LyngPath { +private fun parsePathArg(scope: ScopeFacade, self: ObjPath, arg: Obj): LyngPath { return when (arg) { is ObjString -> arg.value.toPath() is ObjPath -> arg.path @@ -472,11 +474,11 @@ private fun parsePathArg(scope: Scope, self: ObjPath, arg: Obj): LyngPath { } // Map Fs access denials to Lyng runtime exceptions for script-friendly errors -private suspend inline fun Scope.fsGuard(crossinline block: suspend () -> Obj): Obj { +private suspend inline fun ScopeFacade.fsGuard(crossinline block: suspend () -> Obj): Obj { return try { block() } catch (e: AccessDeniedException) { - raiseError(ObjIllegalOperationException(this, e.reasonDetail ?: "access denied")) + raiseError(ObjIllegalOperationException(requireScope(), e.reasonDetail ?: "access denied")) } } @@ -668,16 +670,17 @@ class ObjFsLinesIterator( } } - private suspend fun ensureBufferFilled(scope: Scope) { + private suspend fun ensureBufferFilled(scope: ScopeFacade) { if (buffer.contains('\n') || exhausted) return + val actualScope = scope.requireScope() // Pull next chunk from the underlying iterator - val it = chunksIterator.invokeInstanceMethod(scope, "iterator") - val hasNext = it.invokeInstanceMethod(scope, "hasNext").toBool() + val it = chunksIterator.invokeInstanceMethod(actualScope, "iterator") + val hasNext = it.invokeInstanceMethod(actualScope, "hasNext").toBool() if (!hasNext) { exhausted = true return } - val next = it.invokeInstanceMethod(scope, "next") + val next = it.invokeInstanceMethod(actualScope, "next") buffer += next.toString() } } diff --git a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/process/LyngProcessModule.kt b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/process/LyngProcessModule.kt index e96dfe0..3c41a16 100644 --- a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/process/LyngProcessModule.kt +++ b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/process/LyngProcessModule.kt @@ -20,10 +20,11 @@ package net.sergeych.lyng.io.process import kotlinx.coroutines.flow.Flow import net.sergeych.lyng.ModuleScope import net.sergeych.lyng.Scope +import net.sergeych.lyng.ScopeFacade +import net.sergeych.lyng.requireScope import net.sergeych.lyng.miniast.* import net.sergeych.lyng.obj.* import net.sergeych.lyng.pacman.ImportManager -import net.sergeych.lyng.statement import net.sergeych.lyngio.process.* import net.sergeych.lyngio.process.security.ProcessAccessDeniedException import net.sergeych.lyngio.process.security.ProcessAccessPolicy @@ -205,20 +206,21 @@ class ObjRunningProcess( override fun toString(): String = "RunningProcess($process)" } -private suspend inline fun Scope.processGuard(crossinline block: suspend () -> Obj): Obj { +private suspend inline fun ScopeFacade.processGuard(crossinline block: suspend () -> Obj): Obj { return try { block() } catch (e: ProcessAccessDeniedException) { - raiseError(ObjIllegalOperationException(this, e.reasonDetail ?: "process access denied")) + raiseError(ObjIllegalOperationException(requireScope(), e.reasonDetail ?: "process access denied")) } catch (e: Exception) { - raiseError(ObjIllegalOperationException(this, e.message ?: "process error")) + raiseError(ObjIllegalOperationException(requireScope(), e.message ?: "process error")) } } -private fun Flow.toLyngFlow(flowScope: Scope): ObjFlow { - val producer = statement { - val builder = (this as? net.sergeych.lyng.ClosureScope)?.callScope?.thisObj as? ObjFlowBuilder - ?: this.thisObj as? ObjFlowBuilder +private fun Flow.toLyngFlow(flowScope: ScopeFacade): ObjFlow { + val producer = net.sergeych.lyng.obj.ObjExternCallable.fromBridge { + val scope = requireScope() + val builder = (scope as? net.sergeych.lyng.BytecodeClosureScope)?.callScope?.thisObj as? ObjFlowBuilder + ?: scope.thisObj as? ObjFlowBuilder this@toLyngFlow.collect { try { @@ -230,5 +232,5 @@ private fun Flow.toLyngFlow(flowScope: Scope): ObjFlow { } ObjVoid } - return ObjFlow(producer, flowScope) + return ObjFlow(producer, flowScope.requireScope()) } diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index e51138f..daf1234 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.2.1" +version = "1.5.0-SNAPSHOT" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt index c5fb7c2..e15992f 100644 --- a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt +++ b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt @@ -42,4 +42,4 @@ actual object PerfDefaults { actual val ARG_SMALL_ARITY_12: Boolean = false actual val INDEX_PIC_SIZE_4: Boolean = false actual val RANGE_FAST_ITER: Boolean = false -} \ No newline at end of file +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt index e4b035c..f4d0348 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -20,6 +20,7 @@ package net.sergeych.lyng import net.sergeych.lyng.miniast.MiniTypeRef import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.ObjList +import net.sergeych.lyng.obj.ObjNull import net.sergeych.lyng.obj.ObjRecord /** @@ -61,30 +62,59 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) } } if (!hasComplex) { - if (arguments.list.size != params.size) + if (arguments.list.size > params.size) scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}") + if (arguments.list.size < params.size) { + for (i in arguments.list.size until params.size) { + val a = params[i] + if (!a.type.isNullable) { + scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}") + } + } + } for (i in params.indices) { val a = params[i] - val value = arguments.list[i] - scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, + val value = if (i < arguments.list.size) arguments.list[i] else ObjNull + val recordType = if (declaringClass != null && a.accessType != null) { + ObjRecord.Type.ConstructorField + } else { + ObjRecord.Type.Argument + } + scope.addItem( + a.name, + (a.accessType ?: defaultAccessType).isMutable, value.byValueCopy(), a.visibility ?: defaultVisibility, - recordType = ObjRecord.Type.Argument, + recordType = recordType, declaringClass = declaringClass, - isTransient = a.isTransient) + isTransient = a.isTransient + ) } return } } fun assign(a: Item, value: Obj) { - scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, + val recordType = if (declaringClass != null && a.accessType != null) { + ObjRecord.Type.ConstructorField + } else { + ObjRecord.Type.Argument + } + scope.addItem( + a.name, + (a.accessType ?: defaultAccessType).isMutable, value.byValueCopy(), a.visibility ?: defaultVisibility, - recordType = ObjRecord.Type.Argument, + recordType = recordType, declaringClass = declaringClass, - isTransient = a.isTransient) + isTransient = a.isTransient + ) + } + + suspend fun missingValue(a: Item, error: String): Obj { + return a.defaultValue?.callOn(scope) + ?: if (a.type.isNullable) ObjNull else scope.raiseIllegalArgument(error) } // Prepare positional args and parameter count, handle tail-block binding @@ -165,8 +195,7 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) assign(a, namedValues[i]!!) } else { val value = if (hp < callArgs.size) callArgs[hp++] - else a.defaultValue?.execute(scope) - ?: scope.raiseIllegalArgument("too few arguments for the call (missing ${a.name})") + else missingValue(a, "too few arguments for the call (missing ${a.name})") assign(a, value) } i++ @@ -186,8 +215,7 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) assign(a, namedValues[i]!!) } else { val value = if (tp >= headPosBound) callArgs[tp--] - else a.defaultValue?.execute(scope) - ?: scope.raiseIllegalArgument("too few arguments for the call") + else missingValue(a, "too few arguments for the call") assign(a, value) } i-- @@ -222,10 +250,194 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) } } + /** + * Assign arguments directly into frame slots using [paramSlotPlan] without creating scope locals. + * Default expressions must resolve through frame slots (no scope mirroring). + */ + suspend fun assignToFrame( + scope: Scope, + arguments: Arguments = scope.args, + paramSlotPlan: Map, + frame: FrameAccess, + slotOffset: Int = 0 + ) { + fun slotFor(name: String): Int { + val full = paramSlotPlan[name] ?: scope.raiseIllegalState("parameter slot for '$name' is missing") + val slot = full - slotOffset + if (slot < 0) scope.raiseIllegalState("parameter slot for '$name' is out of range") + return slot + } + + fun setFrameValue(slot: Int, value: Obj) { + when (value) { + is net.sergeych.lyng.obj.ObjInt -> frame.setInt(slot, value.value) + is net.sergeych.lyng.obj.ObjReal -> frame.setReal(slot, value.value) + is net.sergeych.lyng.obj.ObjBool -> frame.setBool(slot, value.value) + else -> frame.setObj(slot, value) + } + } + + fun assign(a: Item, value: Obj) { + val slot = slotFor(a.name) + setFrameValue(slot, value.byValueCopy()) + } + + suspend fun missingValue(a: Item, error: String): Obj { + return a.defaultValue?.callOn(scope) + ?: if (a.type.isNullable) ObjNull else scope.raiseIllegalArgument(error) + } + + // Fast path for simple positional-only calls with no ellipsis and no defaults + if (arguments.named.isEmpty() && !arguments.tailBlockMode) { + var hasComplex = false + for (p in params) { + if (p.isEllipsis || p.defaultValue != null) { + hasComplex = true + break + } + } + if (!hasComplex) { + if (arguments.list.size > params.size) + scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}") + if (arguments.list.size < params.size) { + for (i in arguments.list.size until params.size) { + val a = params[i] + if (!a.type.isNullable) { + scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}") + } + } + } + + for (i in params.indices) { + val a = params[i] + val value = if (i < arguments.list.size) arguments.list[i] else ObjNull + assign(a, value) + } + return + } + } + + // Prepare positional args and parameter count, handle tail-block binding + val callArgs: List + val paramsSize: Int + if (arguments.tailBlockMode) { + val lastParam = params.last() + if (arguments.named.containsKey(lastParam.name)) + scope.raiseIllegalArgument("trailing block cannot be used when the last parameter is already assigned by a named argument") + paramsSize = params.size - 1 + assign(lastParam, arguments.list.last()) + callArgs = arguments.list.dropLast(1) + } else { + paramsSize = params.size + callArgs = arguments.list + } + + val coveredByPositional = BooleanArray(paramsSize) + run { + var headRequired = 0 + var tailRequired = 0 + val ellipsisIdx = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis } + if (ellipsisIdx >= 0) { + for (i in 0 until ellipsisIdx) if (!params[i].isEllipsis && params[i].defaultValue == null) headRequired++ + for (i in paramsSize - 1 downTo ellipsisIdx + 1) if (params[i].defaultValue == null) tailRequired++ + } else { + for (i in 0 until paramsSize) if (params[i].defaultValue == null) headRequired++ + } + val P = callArgs.size + if (ellipsisIdx < 0) { + val k = minOf(P, paramsSize) + for (i in 0 until k) coveredByPositional[i] = true + } else { + val headTake = minOf(P, headRequired) + for (i in 0 until headTake) coveredByPositional[i] = true + val remaining = P - headTake + val tailTake = minOf(remaining, tailRequired) + var j = paramsSize - 1 + var taken = 0 + while (j > ellipsisIdx && taken < tailTake) { + coveredByPositional[j] = true + j-- + taken++ + } + } + } + + val assignedByName = BooleanArray(paramsSize) + val namedValues = arrayOfNulls(paramsSize) + if (arguments.named.isNotEmpty()) { + for ((k, v) in arguments.named) { + val idx = params.subList(0, paramsSize).indexOfFirst { it.name == k } + if (idx < 0) scope.raiseIllegalArgument("unknown parameter '$k'") + if (params[idx].isEllipsis) scope.raiseIllegalArgument("ellipsis (variadic) parameter cannot be assigned by name: '$k'") + if (coveredByPositional[idx]) scope.raiseIllegalArgument("argument '$k' is already set by positional argument") + if (assignedByName[idx]) scope.raiseIllegalArgument("argument '$k' is already set") + assignedByName[idx] = true + namedValues[idx] = v + } + } + + suspend fun processHead(index: Int, headPos: Int): Pair { + var i = index + var hp = headPos + while (i < paramsSize) { + val a = params[i] + if (a.isEllipsis) break + if (assignedByName[i]) { + assign(a, namedValues[i]!!) + } else { + val value = if (hp < callArgs.size) callArgs[hp++] + else missingValue(a, "too few arguments for the call (missing ${a.name})") + assign(a, value) + } + i++ + } + return i to hp + } + + suspend fun processTail(startExclusive: Int, tailStart: Int, headPosBound: Int): Int { + var i = paramsSize - 1 + var tp = tailStart + while (i > startExclusive) { + val a = params[i] + if (a.isEllipsis) break + if (i < assignedByName.size && assignedByName[i]) { + assign(a, namedValues[i]!!) + } else { + val value = if (tp >= headPosBound) callArgs[tp--] + else missingValue(a, "too few arguments for the call") + assign(a, value) + } + i-- + } + return tp + } + + fun processEllipsis(index: Int, headPos: Int, tailPos: Int) { + val a = params[index] + val from = headPos + val to = tailPos + val l = if (from > to) ObjList() + else ObjList(callArgs.subList(from, to + 1).toMutableList()) + assign(a, l) + } + + val ellipsisIndex = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis } + + if (ellipsisIndex >= 0) { + val (_, headConsumedTo) = processHead(0, 0) + val tailConsumedFrom = processTail(ellipsisIndex, callArgs.size - 1, headConsumedTo) + processEllipsis(ellipsisIndex, headConsumedTo, tailConsumedFrom) + } else { + val (_, headConsumedTo) = processHead(0, 0) + if (headConsumedTo != callArgs.size) + scope.raiseIllegalArgument("too many arguments for the call") + } + } + /** * Single argument declaration descriptor. * - * @param defaultValue default value, if set, can't be an [Obj] as it can depend on the call site, call args, etc. + * @param defaultValue default value, callable evaluated at call site. * If not null, could be executed on __caller context__ only. */ data class Item( @@ -238,9 +450,9 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) * Default value, if set, can't be an [Obj] as it can depend on the call site, call args, etc. * So it is a [Statement] that must be executed on __caller context__. */ - val defaultValue: Statement? = null, + val defaultValue: Obj? = null, val accessType: AccessType? = null, val visibility: Visibility? = null, val isTransient: Boolean = false, ) -} \ No newline at end of file +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt index 2791333..61bb174 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt @@ -20,7 +20,7 @@ package net.sergeych.lyng import net.sergeych.lyng.obj.* data class ParsedArgument( - val value: Statement, + val value: Obj, val pos: Pos, val isSplat: Boolean = false, val name: String? = null, @@ -40,115 +40,115 @@ data class ParsedArgument( if (!hasSplatOrNamed && count == this.size) { val quick = when (count) { 0 -> Arguments.EMPTY - 1 -> Arguments(listOf(this.elementAt(0).value.execute(scope)), tailBlockMode) + 1 -> Arguments(listOf(this.elementAt(0).value.callOn(scope)), tailBlockMode) 2 -> { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) Arguments(listOf(a0, a1), tailBlockMode) } 3 -> { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) Arguments(listOf(a0, a1, a2), tailBlockMode) } 4 -> { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) - val a3 = this.elementAt(3).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) + val a3 = this.elementAt(3).value.callOn(scope) Arguments(listOf(a0, a1, a2, a3), tailBlockMode) } 5 -> { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) - val a3 = this.elementAt(3).value.execute(scope) - val a4 = this.elementAt(4).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) + val a3 = this.elementAt(3).value.callOn(scope) + val a4 = this.elementAt(4).value.callOn(scope) Arguments(listOf(a0, a1, a2, a3, a4), tailBlockMode) } 6 -> { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) - val a3 = this.elementAt(3).value.execute(scope) - val a4 = this.elementAt(4).value.execute(scope) - val a5 = this.elementAt(5).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) + val a3 = this.elementAt(3).value.callOn(scope) + val a4 = this.elementAt(4).value.callOn(scope) + val a5 = this.elementAt(5).value.callOn(scope) Arguments(listOf(a0, a1, a2, a3, a4, a5), tailBlockMode) } 7 -> { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) - val a3 = this.elementAt(3).value.execute(scope) - val a4 = this.elementAt(4).value.execute(scope) - val a5 = this.elementAt(5).value.execute(scope) - val a6 = this.elementAt(6).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) + val a3 = this.elementAt(3).value.callOn(scope) + val a4 = this.elementAt(4).value.callOn(scope) + val a5 = this.elementAt(5).value.callOn(scope) + val a6 = this.elementAt(6).value.callOn(scope) Arguments(listOf(a0, a1, a2, a3, a4, a5, a6), tailBlockMode) } 8 -> { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) - val a3 = this.elementAt(3).value.execute(scope) - val a4 = this.elementAt(4).value.execute(scope) - val a5 = this.elementAt(5).value.execute(scope) - val a6 = this.elementAt(6).value.execute(scope) - val a7 = this.elementAt(7).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) + val a3 = this.elementAt(3).value.callOn(scope) + val a4 = this.elementAt(4).value.callOn(scope) + val a5 = this.elementAt(5).value.callOn(scope) + val a6 = this.elementAt(6).value.callOn(scope) + val a7 = this.elementAt(7).value.callOn(scope) Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7), tailBlockMode) } 9 -> if (PerfFlags.ARG_SMALL_ARITY_12) { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) - val a3 = this.elementAt(3).value.execute(scope) - val a4 = this.elementAt(4).value.execute(scope) - val a5 = this.elementAt(5).value.execute(scope) - val a6 = this.elementAt(6).value.execute(scope) - val a7 = this.elementAt(7).value.execute(scope) - val a8 = this.elementAt(8).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) + val a3 = this.elementAt(3).value.callOn(scope) + val a4 = this.elementAt(4).value.callOn(scope) + val a5 = this.elementAt(5).value.callOn(scope) + val a6 = this.elementAt(6).value.callOn(scope) + val a7 = this.elementAt(7).value.callOn(scope) + val a8 = this.elementAt(8).value.callOn(scope) Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8), tailBlockMode) } else null 10 -> if (PerfFlags.ARG_SMALL_ARITY_12) { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) - val a3 = this.elementAt(3).value.execute(scope) - val a4 = this.elementAt(4).value.execute(scope) - val a5 = this.elementAt(5).value.execute(scope) - val a6 = this.elementAt(6).value.execute(scope) - val a7 = this.elementAt(7).value.execute(scope) - val a8 = this.elementAt(8).value.execute(scope) - val a9 = this.elementAt(9).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) + val a3 = this.elementAt(3).value.callOn(scope) + val a4 = this.elementAt(4).value.callOn(scope) + val a5 = this.elementAt(5).value.callOn(scope) + val a6 = this.elementAt(6).value.callOn(scope) + val a7 = this.elementAt(7).value.callOn(scope) + val a8 = this.elementAt(8).value.callOn(scope) + val a9 = this.elementAt(9).value.callOn(scope) Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9), tailBlockMode) } else null 11 -> if (PerfFlags.ARG_SMALL_ARITY_12) { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) - val a3 = this.elementAt(3).value.execute(scope) - val a4 = this.elementAt(4).value.execute(scope) - val a5 = this.elementAt(5).value.execute(scope) - val a6 = this.elementAt(6).value.execute(scope) - val a7 = this.elementAt(7).value.execute(scope) - val a8 = this.elementAt(8).value.execute(scope) - val a9 = this.elementAt(9).value.execute(scope) - val a10 = this.elementAt(10).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) + val a3 = this.elementAt(3).value.callOn(scope) + val a4 = this.elementAt(4).value.callOn(scope) + val a5 = this.elementAt(5).value.callOn(scope) + val a6 = this.elementAt(6).value.callOn(scope) + val a7 = this.elementAt(7).value.callOn(scope) + val a8 = this.elementAt(8).value.callOn(scope) + val a9 = this.elementAt(9).value.callOn(scope) + val a10 = this.elementAt(10).value.callOn(scope) Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10), tailBlockMode) } else null 12 -> if (PerfFlags.ARG_SMALL_ARITY_12) { - val a0 = this.elementAt(0).value.execute(scope) - val a1 = this.elementAt(1).value.execute(scope) - val a2 = this.elementAt(2).value.execute(scope) - val a3 = this.elementAt(3).value.execute(scope) - val a4 = this.elementAt(4).value.execute(scope) - val a5 = this.elementAt(5).value.execute(scope) - val a6 = this.elementAt(6).value.execute(scope) - val a7 = this.elementAt(7).value.execute(scope) - val a8 = this.elementAt(8).value.execute(scope) - val a9 = this.elementAt(9).value.execute(scope) - val a10 = this.elementAt(10).value.execute(scope) - val a11 = this.elementAt(11).value.execute(scope) + val a0 = this.elementAt(0).value.callOn(scope) + val a1 = this.elementAt(1).value.callOn(scope) + val a2 = this.elementAt(2).value.callOn(scope) + val a3 = this.elementAt(3).value.callOn(scope) + val a4 = this.elementAt(4).value.callOn(scope) + val a5 = this.elementAt(5).value.callOn(scope) + val a6 = this.elementAt(6).value.callOn(scope) + val a7 = this.elementAt(7).value.callOn(scope) + val a8 = this.elementAt(8).value.callOn(scope) + val a9 = this.elementAt(9).value.callOn(scope) + val a10 = this.elementAt(10).value.callOn(scope) + val a11 = this.elementAt(11).value.callOn(scope) Arguments(listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11), tailBlockMode) } else null else -> null @@ -166,12 +166,12 @@ data class ParsedArgument( // Named argument if (named == null) named = linkedMapOf() if (named.containsKey(x.name)) scope.raiseIllegalArgument("argument '${x.name}' is already set") - val v = x.value.execute(scope) + val v = x.value.callOn(scope) named[x.name] = v namedSeen = true continue } - val value = x.value.execute(scope) + val value = x.value.callOn(scope) if (x.isSplat) { when { // IMPORTANT: handle ObjMap BEFORE generic Iterable to ensure map splats diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt new file mode 100644 index 0000000..1fdb716 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BlockStatement.kt @@ -0,0 +1,36 @@ +/* + * 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 net.sergeych.lyng.obj.Obj + +class BlockStatement( + val block: Script, + val slotPlan: Map, + val scopeId: Int, + val captureSlots: List = emptyList(), + private val startPos: Pos, +) : Statement() { + override val pos: Pos = startPos + + override suspend fun execute(scope: Scope): Obj { + return bytecodeOnly(scope, "block statement") + } + + fun statements(): List = block.debugStatements() + +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeBodyProvider.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeBodyProvider.kt new file mode 100644 index 0000000..f28bdac --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeBodyProvider.kt @@ -0,0 +1,23 @@ +/* + * 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 net.sergeych.lyng.bytecode.BytecodeStatement + +interface BytecodeBodyProvider { + fun bytecodeBody(): BytecodeStatement? +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt new file mode 100644 index 0000000..3181726 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt @@ -0,0 +1,19 @@ +/* + * 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 + +interface BytecodeCallable diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeExec.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeExec.kt new file mode 100644 index 0000000..b7d5765 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeExec.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * 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 net.sergeych.lyng.bytecode.BytecodeStatement +import net.sergeych.lyng.bytecode.CmdVm +import net.sergeych.lyng.bytecode.seedFrameLocalsFromScope +import net.sergeych.lyng.obj.Obj + +internal suspend fun executeBytecodeWithSeed(scope: Scope, stmt: Statement, label: String): Obj { + val bytecode = when (stmt) { + is BytecodeStatement -> stmt + is BytecodeBodyProvider -> stmt.bytecodeBody() + else -> null + } ?: scope.raiseIllegalState("$label requires bytecode statement") + scope.pos = bytecode.pos + return CmdVm().execute(bytecode.bytecodeFunction(), scope, scope.args) { frame, _ -> + seedFrameLocalsFromScope(frame, scope) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CallSignature.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CallSignature.kt new file mode 100644 index 0000000..6f0fb06 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CallSignature.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * 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 + +/** + * Compile-time call metadata for known functions. Used to select lambda receiver semantics. + */ +data class CallSignature( + val tailBlockReceiverType: String? = null +) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CaptureSlot.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CaptureSlot.kt new file mode 100644 index 0000000..cdc3f38 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CaptureSlot.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * 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 + +data class CaptureSlot( + val name: String, + val ownerScopeId: Int? = null, + val ownerSlot: Int? = null, +) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassDeclStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassDeclStatement.kt new file mode 100644 index 0000000..de06657 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassDeclStatement.kt @@ -0,0 +1,202 @@ +/* + * 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 net.sergeych.lyng.obj.* + +data class ClassDeclBaseSpec( + val name: String, + val args: List? +) + +data class ClassDeclSpec( + val declaredName: String?, + val className: String, + val typeName: String, + val startPos: Pos, + val isExtern: Boolean, + val isAbstract: Boolean, + val isClosed: Boolean, + val isObject: Boolean, + val isAnonymous: Boolean, + val baseSpecs: List, + val constructorArgs: ArgsDeclaration?, + val constructorFieldIds: Map?, + val bodyInit: Statement?, + val initScope: List, +) + +internal suspend fun executeClassDecl( + scope: Scope, + spec: ClassDeclSpec, + bodyCaptureRecords: List? = null, + bodyCaptureNames: List? = null +): Obj { + fun checkClosedParents(parents: List, pos: Pos) { + val closedParent = parents.firstOrNull { it.isClosed } ?: return + throw ScriptError(pos, "can't inherit from closed class ${closedParent.className}") + } + if (spec.isObject) { + val parentClasses = spec.baseSpecs.map { baseSpec -> + val rec = scope[baseSpec.name] ?: throw ScriptError(spec.startPos, "unknown base class: ${baseSpec.name}") + (rec.value as? ObjClass) ?: throw ScriptError(spec.startPos, "${baseSpec.name} is not a class") + } + checkClosedParents(parentClasses, spec.startPos) + + val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray()) + newClass.isAnonymous = spec.isAnonymous + newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN) + for (i in parentClasses.indices) { + val argsList = spec.baseSpecs[i].args + if (argsList != null) newClass.directParentArgs[parentClasses[i]] = argsList + } + + val classScope = scope.createChildScope(newThisObj = newClass) + if (!bodyCaptureRecords.isNullOrEmpty() && !bodyCaptureNames.isNullOrEmpty()) { + classScope.captureRecords = bodyCaptureRecords + classScope.captureNames = bodyCaptureNames + } + classScope.currentClassCtx = newClass + newClass.classScope = classScope + classScope.addConst("object", newClass) + + spec.bodyInit?.let { executeBytecodeWithSeed(classScope, it, "object body init") } + + val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY)) + if (spec.declaredName != null) { + scope.addItem(spec.declaredName, false, instance) + } + return instance + } + + if (spec.isExtern) { + val rec = scope[spec.className] + val existing = rec?.value as? ObjClass + val resolved = if (existing != null) { + existing + } else if (spec.className.contains('.')) { + scope.resolveQualifiedIdentifier(spec.className) as? ObjClass + } else { + null + } + val stub = resolved ?: ObjInstanceClass(spec.className).apply { this.isAbstract = true } + spec.declaredName?.let { scope.addItem(it, false, stub) } + return stub + } + + val parentClasses = spec.baseSpecs.map { baseSpec -> + val rec = scope[baseSpec.name] + val cls = rec?.value as? ObjClass + if (cls != null) return@map cls + if (baseSpec.name == "Exception") return@map ObjException.Root + if (rec == null) throw ScriptError(spec.startPos, "unknown base class: ${baseSpec.name}") + throw ScriptError(spec.startPos, "${baseSpec.name} is not a class") + } + checkClosedParents(parentClasses, spec.startPos) + + val constructorCode = object : Statement() { + override val pos: Pos = spec.startPos + override suspend fun execute(scope: Scope): Obj { + val instance = scope.thisObj as ObjInstance + return instance + } + } + + val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).also { + it.isAbstract = spec.isAbstract + it.isClosed = spec.isClosed + it.instanceConstructor = constructorCode + it.constructorMeta = spec.constructorArgs + for (i in parentClasses.indices) { + val argsList = spec.baseSpecs[i].args + if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList + } + spec.constructorArgs?.params?.forEach { p -> + if (p.accessType != null) { + it.createField( + p.name, + ObjNull, + isMutable = p.accessType == AccessType.Var, + visibility = p.visibility ?: Visibility.Public, + declaringClass = it, + pos = Pos.builtIn, + isTransient = p.isTransient, + type = ObjRecord.Type.ConstructorField, + fieldId = spec.constructorFieldIds?.get(p.name) + ) + } + } + } + + spec.declaredName?.let { name -> + scope.addItem(name, false, newClass) + val module = scope as? ModuleScope + val frame = module?.moduleFrame + if (module != null && frame != null) { + val idx = module.moduleFrameLocalSlotNames.indexOf(name) + if (idx >= 0) { + frame.setObj(idx, newClass) + } + } + } + val classScope = scope.createChildScope(newThisObj = newClass) + if (!bodyCaptureRecords.isNullOrEmpty() && !bodyCaptureNames.isNullOrEmpty()) { + classScope.captureRecords = bodyCaptureRecords + classScope.captureNames = bodyCaptureNames + } + classScope.currentClassCtx = newClass + newClass.classScope = classScope + spec.bodyInit?.let { executeBytecodeWithSeed(classScope, it, "class body init") } + if (spec.initScope.isNotEmpty()) { + for (s in spec.initScope) { + executeBytecodeWithSeed(classScope, s, "class init") + } + } + newClass.checkAbstractSatisfaction(spec.startPos) + return newClass +} + +private suspend fun requireBytecodeBody( + scope: Scope, + stmt: Statement, + label: String +): net.sergeych.lyng.bytecode.BytecodeStatement { + val bytecode = when (stmt) { + is net.sergeych.lyng.bytecode.BytecodeStatement -> stmt + is BytecodeBodyProvider -> stmt.bytecodeBody() + else -> null + } + return bytecode ?: scope.raiseIllegalState("$label requires bytecode statement") +} + +class ClassDeclStatement( + val spec: ClassDeclSpec, +) : Statement() { + override val pos: Pos = spec.startPos + val declaredName: String? get() = spec.declaredName + val typeName: String get() = spec.typeName + + override suspend fun execute(scope: Scope): Obj { + return executeClassDecl(scope, spec) + } + + override suspend fun callOn(scope: Scope): Obj { + val target = scope.parent ?: scope + return executeClassDecl(target, spec) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassInstanceDeclStatements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassInstanceDeclStatements.kt new file mode 100644 index 0000000..fac98d0 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassInstanceDeclStatements.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * 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 net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjProperty + +class ClassInstanceInitDeclStatement( + val initStatement: Statement, + override val pos: Pos, +) : Statement() { + override suspend fun execute(scope: Scope): Obj { + return bytecodeOnly(scope, "class instance init declaration") + } +} + +class ClassInstanceFieldDeclStatement( + val name: String, + val isMutable: Boolean, + val visibility: Visibility, + val writeVisibility: Visibility?, + val isAbstract: Boolean, + val isClosed: Boolean, + val isOverride: Boolean, + val isTransient: Boolean, + val fieldId: Int?, + val initStatement: Statement?, + override val pos: Pos, +) : Statement() { + override suspend fun execute(scope: Scope): Obj { + return bytecodeOnly(scope, "class instance field declaration") + } +} + +class ClassInstancePropertyDeclStatement( + val name: String, + val isMutable: Boolean, + val visibility: Visibility, + val writeVisibility: Visibility?, + val isAbstract: Boolean, + val isClosed: Boolean, + val isOverride: Boolean, + val isTransient: Boolean, + val prop: ObjProperty, + val methodId: Int?, + val initStatement: Statement?, + override val pos: Pos, +) : Statement() { + override suspend fun execute(scope: Scope): Obj { + return bytecodeOnly(scope, "class instance property declaration") + } +} + +class ClassInstanceDelegatedDeclStatement( + val name: String, + val isMutable: Boolean, + val visibility: Visibility, + val writeVisibility: Visibility?, + val isAbstract: Boolean, + val isClosed: Boolean, + val isOverride: Boolean, + val isTransient: Boolean, + val methodId: Int?, + val initStatement: Statement?, + override val pos: Pos, +) : Statement() { + override suspend fun execute(scope: Scope): Obj { + return bytecodeOnly(scope, "class instance delegated declaration") + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassStaticFieldInitStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassStaticFieldInitStatement.kt new file mode 100644 index 0000000..2a857a7 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassStaticFieldInitStatement.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * 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 net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ObjNull +import net.sergeych.lyng.obj.ObjRecord +import net.sergeych.lyng.obj.ObjString +import net.sergeych.lyng.obj.ObjUnset +import net.sergeych.lyng.obj.ObjVoid + +class ClassStaticFieldInitStatement( + val name: String, + val isMutable: Boolean, + val visibility: Visibility, + val writeVisibility: Visibility?, + val initializer: Statement?, + val isDelegated: Boolean, + val isTransient: Boolean, + private val startPos: Pos, +) : Statement() { + override val pos: Pos = startPos + + override suspend fun execute(scope: Scope): Obj { + val initValue = initializer?.let { execBytecodeOnly(scope, it, "class static field init") }?.byValueCopy() + ?: ObjNull + val cls = scope.thisObj as? ObjClass + ?: scope.raiseIllegalState("static field init requires class scope") + return if (isDelegated) { + val accessTypeStr = if (isMutable) "Var" else "Val" + val accessType = ObjString(accessTypeStr) + val finalDelegate = try { + initValue.invokeInstanceMethod( + scope, + "bind", + Arguments(ObjString(name), accessType, scope.thisObj) + ) + } catch (_: Exception) { + initValue + } + cls.createClassField( + name, + ObjUnset, + isMutable, + visibility, + writeVisibility, + startPos, + isTransient = isTransient, + type = ObjRecord.Type.Delegated + ).apply { + delegate = finalDelegate + } + scope.addItem( + name, + isMutable, + ObjUnset, + visibility, + writeVisibility, + recordType = ObjRecord.Type.Delegated, + isTransient = isTransient + ).apply { + delegate = finalDelegate + } + finalDelegate + } else { + cls.createClassField( + name, + initValue, + isMutable, + visibility, + writeVisibility, + startPos, + isTransient = isTransient + ) + scope.addItem( + name, + isMutable, + initValue, + visibility, + writeVisibility, + recordType = ObjRecord.Type.Field, + isTransient = isTransient + ) + initValue + } + } + + private suspend fun execBytecodeOnly(scope: Scope, stmt: Statement, label: String): Obj { + val bytecode = when (stmt) { + is net.sergeych.lyng.bytecode.BytecodeStatement -> stmt + is BytecodeBodyProvider -> stmt.bytecodeBody() + else -> null + } ?: scope.raiseIllegalState("$label requires bytecode statement") + return bytecode.execute(scope) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt index ff891b0..b9d80a5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt @@ -18,67 +18,43 @@ package net.sergeych.lyng import net.sergeych.lyng.obj.Obj -import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjRecord /** - * Scope that adds a "closure" to caller; most often it is used to apply class instance to caller scope. - * Inherits [Scope.args] and [Scope.thisObj] from [callScope] and adds lookup for symbols - * from [closureScope] with proper precedence + * Bytecode-oriented closure scope that keeps the call scope parent chain for stack traces + * while carrying the lexical closure for `this` variants and module resolution. + * Unlike legacy closure scopes, it does not override name lookup. */ -class ClosureScope(val callScope: Scope, val closureScope: Scope) : - // Important: use closureScope.thisObj so unqualified members (e.g., fields) resolve to the instance - // we captured, not to the caller's `this` (e.g., FlowBuilder). +class BytecodeClosureScope( + val callScope: Scope, + val closureScope: Scope, + private val preferredThisType: String? = null +) : Scope(callScope, callScope.args, thisObj = closureScope.thisObj) { init { - // Preserve the lexical class context of the closure by default. This ensures that lambdas - // created inside a class method keep access to that class's private/protected members even - // when executed from within another object's method (e.g., Mutex.withLock), which may set - // its own currentClassCtx temporarily. If the closure has no class context, inherit caller's. + val desired = preferredThisType?.let { typeName -> + callScope.thisVariants.firstOrNull { it.objClass.className == typeName } + } + val primaryThis = closureScope.thisObj + val merged = ArrayList(callScope.thisVariants.size + closureScope.thisVariants.size + 1) + desired?.let { merged.add(it) } + merged.addAll(callScope.thisVariants) + merged.addAll(closureScope.thisVariants) + setThisVariants(primaryThis, merged) this.currentClassCtx = closureScope.currentClassCtx ?: callScope.currentClassCtx } - - override fun get(name: String): ObjRecord? { - if (name == "this") return thisObj.asReadonly - - // 1. Current frame locals (parameters, local variables) - tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } - - // 2. Lexical environment (captured locals from entire ancestry) - closureScope.chainLookupIgnoreClosure(name, followClosure = true, caller = currentClassCtx)?.let { return it } - - // 3. Lexical this members (captured receiver) - val receiver = thisObj - val effectiveClass = receiver as? ObjClass ?: receiver.objClass - for (cls in effectiveClass.mro) { - val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) - if (rec != null && !rec.isAbstract) { - if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx, name)) { - return rec.copy(receiver = receiver) - } - } - } - // Finally, root object fallback - Obj.rootObjectType.members[name]?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) { - return rec.copy(receiver = receiver) - } - } - - // 4. Call environment (caller locals, caller this, and global fallback) - return callScope.get(name) - } } -class ApplyScope(_parent: Scope,val applied: Scope) : Scope(_parent, thisObj = applied.thisObj) { +class ApplyScope(val callScope: Scope, val applied: Scope) : + Scope(callScope, thisObj = applied.thisObj) { override fun get(name: String): ObjRecord? { return applied.get(name) ?: super.get(name) } - override fun applyClosure(closure: Scope): Scope { - return this + override fun applyClosure(closure: Scope, preferredThisType: String?): Scope { + return BytecodeClosureScope(this, closure, preferredThisType) } -} \ No newline at end of file +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt index b01d701..3a6c6fc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt @@ -19,8 +19,24 @@ package net.sergeych.lyng sealed class CodeContext { class Module(@Suppress("unused") val packageName: String?): CodeContext() - class Function(val name: String): CodeContext() + class Function( + val name: String, + val implicitThisMembers: Boolean = false, + val implicitThisTypeName: String? = null, + val typeParams: Set = emptySet(), + val typeParamDecls: List = emptyList() + ): CodeContext() class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() { + var typeParams: Set = emptySet() + var typeParamDecls: List = emptyList() val pendingInitializations = mutableMapOf() + val declaredMembers = mutableSetOf() + val classScopeMembers = mutableSetOf() + val memberOverrides = mutableMapOf() + val memberFieldIds = mutableMapOf() + val memberMethodIds = mutableMapOf() + var nextFieldId: Int = 0 + var nextMethodId: Int = 0 + var slotPlanId: Int? = null } -} \ No newline at end of file +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index a3e858a..7e60e22 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -18,10 +18,12 @@ package net.sergeych.lyng import net.sergeych.lyng.Compiler.Companion.compile +import net.sergeych.lyng.bytecode.* import net.sergeych.lyng.miniast.* import net.sergeych.lyng.obj.* import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportProvider +import net.sergeych.lyng.resolution.* /** * The LYNG compiler. @@ -41,41 +43,543 @@ class Compiler( // Track identifiers known to be locals/parameters in the current function for fast local emission private val localNamesStack = mutableListOf>() + private val localShadowedNamesStack = mutableListOf>() private val currentLocalNames: MutableSet? get() = localNamesStack.lastOrNull() + private val currentShadowedLocalNames: MutableSet? + get() = localShadowedNamesStack.lastOrNull() - private data class SlotPlan(val slots: MutableMap, var nextIndex: Int) - private data class SlotLocation(val slot: Int, val depth: Int) + private data class SlotEntry(val index: Int, val isMutable: Boolean, val isDelegated: Boolean) + private data class SlotPlan(val slots: MutableMap, var nextIndex: Int, val id: Int) + private data class SlotLocation( + val slot: Int, + val depth: Int, + val scopeId: Int, + val isMutable: Boolean, + val isDelegated: Boolean + ) private val slotPlanStack = mutableListOf() + private var nextScopeId = 0 + private val genericFunctionDeclsStack = mutableListOf>(mutableMapOf()) // Track declared local variables count per function for precise capacity hints private val localDeclCountStack = mutableListOf() private val currentLocalDeclCount: Int get() = localDeclCountStack.lastOrNull() ?: 0 + private data class GenericFunctionDecl( + val typeParams: List, + val params: List, + val pos: Pos + ) + + private fun pushGenericFunctionScope() { + genericFunctionDeclsStack.add(mutableMapOf()) + } + + private fun popGenericFunctionScope() { + genericFunctionDeclsStack.removeLast() + } + + private fun currentGenericFunctionDecls(): MutableMap { + return genericFunctionDeclsStack.last() + } + + private fun lookupGenericFunctionDecl(name: String): GenericFunctionDecl? { + for (i in genericFunctionDeclsStack.indices.reversed()) { + genericFunctionDeclsStack[i][name]?.let { return it } + } + return null + } + private inline fun withLocalNames(names: Set, block: () -> T): T { localNamesStack.add(names.toMutableSet()) + localShadowedNamesStack.add(mutableSetOf()) return try { block() } finally { + localShadowedNamesStack.removeLast() localNamesStack.removeLast() } } - private fun declareLocalName(name: String) { + private fun declareLocalName(name: String, isMutable: Boolean, isDelegated: Boolean = false) { // Add to current function's local set; only count if it was newly added (avoid duplicates) val added = currentLocalNames?.add(name) == true + if (!added) { + currentShadowedLocalNames?.add(name) + } + if (added) { + scopeSeedNames.remove(name) + } if (added && localDeclCountStack.isNotEmpty()) { localDeclCountStack[localDeclCountStack.lastIndex] = currentLocalDeclCount + 1 } - declareSlotName(name) + capturePlanStack.lastOrNull()?.let { plan -> + if (plan.captureMap.remove(name) != null) { + plan.captureOwners.remove(name) + plan.captures.removeAll { it.name == name } + } + } + declareSlotName(name, isMutable, isDelegated) } - private fun declareSlotName(name: String) { + private fun declareSlotName(name: String, isMutable: Boolean, isDelegated: Boolean) { + if (codeContexts.lastOrNull() is CodeContext.ClassBody) return val plan = slotPlanStack.lastOrNull() ?: return if (plan.slots.containsKey(name)) return - plan.slots[name] = plan.nextIndex + plan.slots[name] = SlotEntry(plan.nextIndex, isMutable, isDelegated) plan.nextIndex += 1 + if (!seedingSlotPlan && plan == moduleSlotPlan()) { + moduleDeclaredNames.add(name) + } + } + + private fun declareSlotNameIn(plan: SlotPlan, name: String, isMutable: Boolean, isDelegated: Boolean) { + if (plan.slots.containsKey(name)) return + plan.slots[name] = SlotEntry(plan.nextIndex, isMutable, isDelegated) + plan.nextIndex += 1 + } + + private fun declareSlotNameAt( + plan: SlotPlan, + name: String, + slotIndex: Int, + isMutable: Boolean, + isDelegated: Boolean + ) { + if (plan.slots.containsKey(name)) return + plan.slots[name] = SlotEntry(slotIndex, isMutable, isDelegated) + if (slotIndex >= plan.nextIndex) { + plan.nextIndex = slotIndex + 1 + } + } + + private fun moduleSlotPlan(): SlotPlan? = slotPlanStack.firstOrNull() + private val slotTypeByScopeId: MutableMap> = mutableMapOf() + private val nameObjClass: MutableMap = mutableMapOf() + private val scopeSeedNames: MutableSet = mutableSetOf() + private val slotTypeDeclByScopeId: MutableMap> = mutableMapOf() + private val nameTypeDecl: MutableMap = mutableMapOf() + private data class TypeAliasDecl( + val name: String, + val typeParams: List, + val body: TypeDecl, + val pos: Pos + ) + private val typeAliases: MutableMap = mutableMapOf() + private val methodReturnTypeDeclByRef: MutableMap = mutableMapOf() + private val callableReturnTypeByScopeId: MutableMap> = mutableMapOf() + private val callableReturnTypeByName: MutableMap = mutableMapOf() + private val lambdaReturnTypeByRef: MutableMap = mutableMapOf() + private val lambdaCaptureEntriesByRef: MutableMap> = + mutableMapOf() + private val classFieldTypesByName: MutableMap> = mutableMapOf() + private val classScopeMembersByClassName: MutableMap> = mutableMapOf() + private val classScopeCallableMembersByClassName: MutableMap> = mutableMapOf() + private val encodedPayloadTypeByScopeId: MutableMap> = mutableMapOf() + private val encodedPayloadTypeByName: MutableMap = mutableMapOf() + private val objectDeclNames: MutableSet = mutableSetOf() + private val externCallableNames: MutableSet = mutableSetOf() + private val moduleDeclaredNames: MutableSet = mutableSetOf() + private var seedingSlotPlan: Boolean = false + + private fun moduleForcedLocalSlotInfo(): Map { + val plan = moduleSlotPlan() ?: return emptyMap() + if (plan.slots.isEmpty()) return emptyMap() + val result = LinkedHashMap(plan.slots.size) + for ((name, entry) in plan.slots) { + result[name] = ForcedLocalSlotInfo( + index = entry.index, + isMutable = entry.isMutable, + isDelegated = entry.isDelegated + ) + } + return result + } + + private fun seedSlotPlanFromScope(scope: Scope, includeParents: Boolean = false) { + val plan = moduleSlotPlan() ?: return + seedingSlotPlan = true + try { + var current: Scope? = scope + while (current != null) { + for ((name, record) in current.objects) { + if (!record.visibility.isPublic) continue + if (plan.slots.containsKey(name)) continue + declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated) + scopeSeedNames.add(name) + val instance = record.value as? ObjInstance + if (instance != null && nameObjClass[name] == null) { + nameObjClass[name] = instance.objClass + } + } + for ((cls, map) in current.extensions) { + for ((name, record) in map) { + if (!record.visibility.isPublic) continue + when (record.type) { + ObjRecord.Type.Property -> { + val getterName = extensionPropertyGetterName(cls.className, name) + if (!plan.slots.containsKey(getterName)) { + declareSlotNameIn( + plan, + getterName, + isMutable = false, + isDelegated = false + ) + scopeSeedNames.add(getterName) + } + val prop = record.value as? ObjProperty + if (prop?.setter != null) { + val setterName = extensionPropertySetterName(cls.className, name) + if (!plan.slots.containsKey(setterName)) { + declareSlotNameIn( + plan, + setterName, + isMutable = false, + isDelegated = false + ) + scopeSeedNames.add(setterName) + } + } + } + else -> { + val callableName = extensionCallableName(cls.className, name) + if (!plan.slots.containsKey(callableName)) { + declareSlotNameIn( + plan, + callableName, + isMutable = false, + isDelegated = false + ) + scopeSeedNames.add(callableName) + } + } + } + } + } + for ((name, slotIndex) in current.slotNameToIndexSnapshot()) { + val record = current.getSlotRecord(slotIndex) + if (!record.visibility.isPublic) continue + if (plan.slots.containsKey(name)) continue + declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated) + scopeSeedNames.add(name) + } + if (!includeParents) return + current = current.parent + } + } finally { + seedingSlotPlan = false + } + } + + private fun seedSlotPlanFromSeedScope(scope: Scope) { + val plan = moduleSlotPlan() ?: return + for ((name, slotIndex) in scope.slotNameToIndexSnapshot()) { + val record = scope.getSlotRecord(slotIndex) + declareSlotNameAt( + plan, + name, + slotIndex, + record.isMutable, + record.type == ObjRecord.Type.Delegated + ) + scopeSeedNames.add(name) + } + } + + private fun predeclareTopLevelSymbols() { + val plan = moduleSlotPlan() ?: return + val saved = cc.savePos() + var depth = 0 + var parenDepth = 0 + var bracketDepth = 0 + fun nextNonWs(): Token { + var t = cc.next() + while (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { + t = cc.next() + } + return t + } + try { + while (cc.hasNext()) { + val t = cc.next() + when (t.type) { + Token.Type.LBRACE -> depth++ + Token.Type.RBRACE -> if (depth > 0) depth-- + Token.Type.LPAREN -> parenDepth++ + Token.Type.RPAREN -> if (parenDepth > 0) parenDepth-- + Token.Type.LBRACKET -> bracketDepth++ + Token.Type.RBRACKET -> if (bracketDepth > 0) bracketDepth-- + Token.Type.ID -> if (depth == 0) { + if (parenDepth > 0 || bracketDepth > 0) continue + when (t.value) { + "fun", "fn" -> { + val nameToken = nextNonWs() + if (nameToken.type != Token.Type.ID) continue + val afterName = cc.peekNextNonWhitespace() + if (afterName.type == Token.Type.DOT) { + cc.nextNonWhitespace() + val actual = cc.nextNonWhitespace() + if (actual.type == Token.Type.ID) { + extensionNames.add(actual.value) + registerExtensionName(nameToken.value, actual.value) + declareSlotNameIn(plan, extensionCallableName(nameToken.value, actual.value), isMutable = false, isDelegated = false) + } + continue + } + declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false) + } + "val", "var" -> { + val nameToken = nextNonWs() + if (nameToken.type != Token.Type.ID) continue + val afterName = cc.peekNextNonWhitespace() + if (afterName.type == Token.Type.DOT) { + cc.nextNonWhitespace() + val actual = cc.nextNonWhitespace() + if (actual.type == Token.Type.ID) { + extensionNames.add(actual.value) + registerExtensionName(nameToken.value, actual.value) + declareSlotNameIn(plan, extensionPropertyGetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false) + if (t.value == "var") { + declareSlotNameIn(plan, extensionPropertySetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false) + } + } + continue + } + declareSlotNameIn(plan, nameToken.value, isMutable = t.value == "var", isDelegated = false) + } + "class", "object" -> { + val nameToken = nextNonWs() + if (nameToken.type == Token.Type.ID) { + declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false) + scopeSeedNames.add(nameToken.value) + } + } + "enum" -> { + val next = nextNonWs() + val nameToken = if (next.type == Token.Type.ID && next.value == "class") nextNonWs() else next + if (nameToken.type == Token.Type.ID) { + declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false) + scopeSeedNames.add(nameToken.value) + } + } + } + } + else -> {} + } + } + } finally { + cc.restorePos(saved) + } + } + + private fun predeclareClassMembers(target: MutableSet, overrides: MutableMap) { + val saved = cc.savePos() + var depth = 0 + val modifiers = setOf( + "public", "private", "protected", "internal", + "override", "abstract", "extern", "static", "transient" + ) + fun nextNonWs(): Token { + var t = cc.next() + while (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { + t = cc.next() + } + return t + } + try { + while (cc.hasNext()) { + var t = cc.next() + when (t.type) { + Token.Type.LBRACE -> depth++ + Token.Type.RBRACE -> if (depth == 0) break else depth-- + Token.Type.ID -> if (depth == 0) { + var sawOverride = false + while (t.type == Token.Type.ID && t.value in modifiers) { + if (t.value == "override") sawOverride = true + t = nextNonWs() + } + when (t.value) { + "fun", "fn", "val", "var" -> { + val nameToken = nextNonWs() + if (nameToken.type == Token.Type.ID) { + val afterName = cc.peekNextNonWhitespace() + if (afterName.type != Token.Type.DOT) { + target.add(nameToken.value) + overrides[nameToken.value] = sawOverride + } + } + } + } + } + else -> {} + } + } + } finally { + cc.restorePos(saved) + } + } + + private fun predeclareClassScopeMembers( + className: String, + target: MutableSet, + callableTarget: MutableSet + ) { + val saved = cc.savePos() + var depth = 0 + val modifiers = setOf( + "public", "private", "protected", "internal", + "override", "abstract", "extern", "static", "transient", "open", "closed" + ) + fun nextNonWs(): Token { + var t = cc.next() + while (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { + t = cc.next() + } + return t + } + try { + while (cc.hasNext()) { + var t = cc.next() + when (t.type) { + Token.Type.LBRACE -> depth++ + Token.Type.RBRACE -> if (depth == 0) break else depth-- + Token.Type.ID -> if (depth == 0) { + var sawStatic = false + while (t.type == Token.Type.ID && t.value in modifiers) { + if (t.value == "static") sawStatic = true + t = nextNonWs() + } + when (t.value) { + "class" -> { + val nameToken = nextNonWs() + if (nameToken.type == Token.Type.ID) { + target.add(nameToken.value) + callableTarget.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) + registerClassScopeCallableMember(className, nameToken.value) + } + } + "object" -> { + val nameToken = nextNonWs() + if (nameToken.type == Token.Type.ID) { + target.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) + } + } + "enum" -> { + val next = nextNonWs() + val nameToken = if (next.type == Token.Type.ID && next.value == "class") nextNonWs() else next + if (nameToken.type == Token.Type.ID) { + target.add(nameToken.value) + callableTarget.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) + registerClassScopeCallableMember(className, nameToken.value) + } + } + "type" -> { + val nameToken = nextNonWs() + if (nameToken.type == Token.Type.ID) { + target.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) + } + } + "fun", "fn", "val", "var" -> { + if (sawStatic) { + val nameToken = nextNonWs() + if (nameToken.type == Token.Type.ID) { + target.add(nameToken.value) + registerClassScopeMember(className, nameToken.value) + } + } + } + } + } + else -> {} + } + } + } finally { + cc.restorePos(saved) + } + } + + private fun registerClassScopeMember(className: String, name: String) { + classScopeMembersByClassName.getOrPut(className) { mutableSetOf() }.add(name) + } + + private fun registerClassScopeCallableMember(className: String, name: String) { + classScopeCallableMembersByClassName.getOrPut(className) { mutableSetOf() }.add(name) + } + + private fun isClassScopeCallableMember(className: String, name: String): Boolean { + return classScopeCallableMembersByClassName[className]?.contains(name) == true + } + + private fun registerClassScopeFieldType(ownerClassName: String?, memberName: String, memberClassName: String) { + if (ownerClassName == null) return + val memberClass = resolveClassByName(memberClassName) ?: return + classFieldTypesByName.getOrPut(ownerClassName) { mutableMapOf() }[memberName] = memberClass + } + + private fun resolveCompileClassInfo(name: String): CompileClassInfo? { + compileClassInfos[name]?.let { return it } + val scopeRec = seedScope?.get(name) ?: importManager.rootScope.get(name) + val clsFromScope = scopeRec?.value as? ObjClass + val clsFromImports = if (clsFromScope == null) { + importedModules.asReversed().firstNotNullOfOrNull { it.scope.get(name)?.value as? ObjClass } + } else { + null + } + val cls = clsFromScope ?: clsFromImports ?: resolveClassByName(name) ?: return null + val fieldIds = cls.instanceFieldIdMap() + val methodIds = cls.instanceMethodIdMap(includeAbstract = true) + val baseNames = cls.directParents.map { it.className } + val nextFieldId = (fieldIds.values.maxOrNull() ?: -1) + 1 + val nextMethodId = (methodIds.values.maxOrNull() ?: -1) + 1 + return CompileClassInfo(name, fieldIds, methodIds, nextFieldId, nextMethodId, baseNames) + } + + private data class BaseMemberIds( + val fieldIds: Map, + val methodIds: Map, + val fieldConflicts: Set, + val methodConflicts: Set, + val nextFieldId: Int, + val nextMethodId: Int + ) + + private fun collectBaseMemberIds(baseNames: List): BaseMemberIds { + val allBaseNames = if (baseNames.contains("Object")) baseNames else baseNames + "Object" + val fieldIds = mutableMapOf() + val methodIds = mutableMapOf() + val fieldConflicts = mutableSetOf() + val methodConflicts = mutableSetOf() + var maxFieldId = -1 + var maxMethodId = -1 + for (base in allBaseNames) { + val info = resolveCompileClassInfo(base) ?: continue + for ((name, id) in info.fieldIds) { + val prev = fieldIds[name] + if (prev == null) fieldIds[name] = id + else if (prev != id) fieldConflicts.add(name) + if (id > maxFieldId) maxFieldId = id + } + for ((name, id) in info.methodIds) { + val prev = methodIds[name] + if (prev == null) methodIds[name] = id + else if (prev != id) methodConflicts.add(name) + if (id > maxMethodId) maxMethodId = id + } + } + return BaseMemberIds( + fieldIds = fieldIds, + methodIds = methodIds, + fieldConflicts = fieldConflicts, + methodConflicts = methodConflicts, + nextFieldId = maxFieldId + 1, + nextMethodId = maxMethodId + 1 + ) } private fun buildParamSlotPlan(names: List): SlotPlan { @@ -87,26 +591,545 @@ class Compiler( idx++ } } - return SlotPlan(map, idx) + val entries = mutableMapOf() + for ((name, index) in map) { + entries[name] = SlotEntry(index, isMutable = false, isDelegated = false) + } + return SlotPlan(entries, idx, nextScopeId++) } - private fun lookupSlotLocation(name: String): SlotLocation? { - for (i in slotPlanStack.indices.reversed()) { - val slot = slotPlanStack[i].slots[name] ?: continue - val depth = slotPlanStack.size - 1 - i - return SlotLocation(slot, depth) + private fun markDelegatedSlot(name: String) { + val plan = slotPlanStack.lastOrNull() ?: return + val entry = plan.slots[name] ?: return + if (!entry.isDelegated) { + plan.slots[name] = entry.copy(isDelegated = true) + } + } + + private fun slotPlanIndices(plan: SlotPlan): Map { + if (plan.slots.isEmpty()) return emptyMap() + val result = LinkedHashMap(plan.slots.size) + for ((name, entry) in plan.slots) { + result[name] = entry.index + } + return result + } + + private fun callSignatureForName(name: String): CallSignature? { + seedScope?.getLocalRecordDirect(name)?.callSignature?.let { return it } + return seedScope?.get(name)?.callSignature + ?: importManager.rootScope.getLocalRecordDirect(name)?.callSignature + } + + internal data class MemberIds(val fieldId: Int?, val methodId: Int?) + + private fun resolveMemberIds(name: String, pos: Pos, qualifier: String? = null): MemberIds { + val ctx = if (qualifier == null) { + codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + } else null + if (ctx != null) { + val fieldId = ctx.memberFieldIds[name] + val methodId = ctx.memberMethodIds[name] + if (fieldId == null && methodId == null) { + if (allowUnresolvedRefs) return MemberIds(null, null) + throw ScriptError(pos, "unknown member $name") + } + return MemberIds(fieldId, methodId) + } + if (qualifier != null) { + val classCtx = codeContexts.asReversed() + .firstOrNull { it is CodeContext.ClassBody && it.name == qualifier } as? CodeContext.ClassBody + if (classCtx != null) { + val fieldId = classCtx.memberFieldIds[name] + val methodId = classCtx.memberMethodIds[name] + if (fieldId != null || methodId != null) return MemberIds(fieldId, methodId) + } + val info = resolveCompileClassInfo(qualifier) + ?: if (allowUnresolvedRefs) return MemberIds(null, null) else throw ScriptError(pos, "unknown type $qualifier") + val fieldId = info.fieldIds[name] + val methodId = info.methodIds[name] + if (fieldId == null && methodId == null) { + if (allowUnresolvedRefs) return MemberIds(null, null) + throw ScriptError(pos, "unknown member $name on $qualifier") + } + return MemberIds(fieldId, methodId) + } + if (allowUnresolvedRefs) return MemberIds(null, null) + throw ScriptError(pos, "member $name is not available without class context") + } + + private fun tailBlockReceiverType(left: ObjRef): String? { + val name = when (left) { + is LocalVarRef -> left.name + is LocalSlotRef -> left.name + is ImplicitThisMemberRef -> left.name + else -> null + } + if (name == null) return null + val signature = callSignatureForName(name) + return signature?.tailBlockReceiverType ?: if (name == "flow") "FlowBuilder" else null + } + + private fun currentImplicitThisTypeName(): String? { + for (ctx in codeContexts.asReversed()) { + val fn = ctx as? CodeContext.Function ?: continue + if (fn.implicitThisTypeName != null) return fn.implicitThisTypeName } return null } + private fun implicitReceiverTypeForMember(name: String): String? { + for (ctx in codeContexts.asReversed()) { + val fn = ctx as? CodeContext.Function ?: continue + if (!fn.implicitThisMembers) continue + val typeName = fn.implicitThisTypeName ?: continue + if (hasImplicitThisMember(name, typeName)) return typeName + } + return null + } + + private fun currentEnclosingClassName(): String? { + val ctx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + return ctx?.name + } + + private fun currentTypeParams(): Set { + val result = mutableSetOf() + pendingTypeParamStack.lastOrNull()?.let { result.addAll(it) } + for (ctx in codeContexts.asReversed()) { + when (ctx) { + is CodeContext.Function -> result.addAll(ctx.typeParams) + is CodeContext.ClassBody -> result.addAll(ctx.typeParams) + else -> {} + } + } + return result + } + + private val pendingTypeParamStack = mutableListOf>() + + private fun parseTypeParamList(): List { + if (cc.peekNextNonWhitespace().type != Token.Type.LT) return emptyList() + val typeParams = mutableListOf() + cc.nextNonWhitespace() + while (true) { + val varianceToken = cc.peekNextNonWhitespace() + val variance = when (varianceToken.value) { + "in" -> { + cc.nextNonWhitespace() + TypeDecl.Variance.In + } + "out" -> { + cc.nextNonWhitespace() + TypeDecl.Variance.Out + } + else -> TypeDecl.Variance.Invariant + } + val idTok = cc.requireToken(Token.Type.ID, "type parameter name expected") + var bound: TypeDecl? = null + var defaultType: TypeDecl? = null + if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) { + bound = parseTypeExpressionWithMini().first + } + if (cc.skipTokenOfType(Token.Type.ASSIGN, isOptional = true)) { + defaultType = parseTypeExpressionWithMini().first + } + typeParams.add(TypeDecl.TypeParam(idTok.value, variance, bound, defaultType)) + val sep = cc.nextNonWhitespace() + when (sep.type) { + Token.Type.COMMA -> continue + Token.Type.GT -> break + Token.Type.SHR -> { + cc.pushPendingGT() + break + } + else -> sep.raiseSyntax("expected ',' or '>' in type parameter list") + } + } + return typeParams + } + + private fun looksLikeTypeAliasDeclaration(): Boolean { + val saved = cc.savePos() + try { + val nameTok = cc.nextNonWhitespace() + if (nameTok.type != Token.Type.ID) return false + val afterName = cc.savePos() + if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) { + var depth = 1 + while (depth > 0) { + val t = cc.nextNonWhitespace() + when (t.type) { + Token.Type.LT -> depth += 1 + Token.Type.GT -> depth -= 1 + Token.Type.SHR -> { + cc.pushPendingGT() + depth -= 1 + } + Token.Type.EOF -> return false + else -> {} + } + } + } else { + cc.restorePos(afterName) + } + cc.skipWsTokens() + return cc.peekNextNonWhitespace().type == Token.Type.ASSIGN + } finally { + cc.restorePos(saved) + } + } + + private suspend fun parseTypeAliasDeclaration(): Statement { + val nameToken = cc.requireToken(Token.Type.ID, "type alias name expected") + val startPos = pendingDeclStart ?: nameToken.pos + val doc = pendingDeclDoc ?: consumePendingDoc() + pendingDeclDoc = null + pendingDeclStart = null + val declaredName = nameToken.value + val outerClassName = currentEnclosingClassName() + val qualifiedName = if (outerClassName != null) "$outerClassName.$declaredName" else declaredName + if (typeAliases.containsKey(qualifiedName)) { + throw ScriptError(nameToken.pos, "type alias $qualifiedName already declared") + } + if (resolveTypeDeclObjClass(TypeDecl.Simple(qualifiedName, false)) != null) { + throw ScriptError(nameToken.pos, "type alias $qualifiedName conflicts with existing class") + } + val typeParams = parseTypeParamList() + val uniqueParams = typeParams.map { it.name }.toSet() + if (uniqueParams.size != typeParams.size) { + throw ScriptError(nameToken.pos, "type alias $qualifiedName has duplicate type parameters") + } + val typeParamNames = uniqueParams + if (typeParamNames.isNotEmpty()) pendingTypeParamStack.add(typeParamNames) + val (body, bodyMini) = try { + cc.skipWsTokens() + val eq = cc.nextNonWhitespace() + if (eq.type != Token.Type.ASSIGN) { + throw ScriptError(eq.pos, "type alias $qualifiedName expects '='") + } + parseTypeExpressionWithMini() + } finally { + if (typeParamNames.isNotEmpty()) pendingTypeParamStack.removeLast() + } + val alias = TypeAliasDecl(qualifiedName, typeParams, body, nameToken.pos) + typeAliases[qualifiedName] = alias + declareLocalName(declaredName, isMutable = false) + resolutionSink?.declareSymbol(declaredName, SymbolKind.LOCAL, isMutable = false, pos = nameToken.pos) + if (outerClassName != null) { + val outerCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + outerCtx?.classScopeMembers?.add(declaredName) + registerClassScopeMember(outerClassName, declaredName) + } + miniSink?.onTypeAliasDecl( + MiniTypeAliasDecl( + range = MiniRange(startPos, cc.currentPos()), + name = declaredName, + typeParams = typeParams.map { it.name }, + target = bodyMini, + doc = doc, + nameStart = nameToken.pos + ) + ) + + val aliasExpr = net.sergeych.lyng.obj.TypeDeclRef(body, nameToken.pos) + val initStmt = ExpressionStatement(aliasExpr, nameToken.pos) + val slotPlan = slotPlanStack.lastOrNull() + val slotIndex = slotPlan?.slots?.get(declaredName)?.index + val scopeId = slotPlan?.id + return VarDeclStatement( + name = declaredName, + isMutable = false, + visibility = Visibility.Public, + initializer = initStmt, + isTransient = false, + slotIndex = slotIndex, + scopeId = scopeId, + startPos = nameToken.pos, + initializerObjClass = null + ) + } + + private fun lookupSlotLocation(name: String, includeModule: Boolean = true): SlotLocation? { + for (i in slotPlanStack.indices.reversed()) { + if (!includeModule && i == 0) continue + val slot = slotPlanStack[i].slots[name] ?: continue + val depth = slotPlanStack.size - 1 - i + return SlotLocation(slot.index, depth, slotPlanStack[i].id, slot.isMutable, slot.isDelegated) + } + return null + } + + private fun resolveIdentifierRef(name: String, pos: Pos): ObjRef { + if (name == "__PACKAGE__") { + resolutionSink?.reference(name, pos) + val value = ObjString(packageName ?: "unknown").asReadonly + return ConstRef(value) + } + if (name == "this") { + resolutionSink?.reference(name, pos) + return LocalVarRef(name, pos) + } + val slotLoc = lookupSlotLocation(name, includeModule = false) + if (slotLoc != null) { + val classCtx = codeContexts.lastOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + if (classCtx?.slotPlanId == slotLoc.scopeId && + classCtx.declaredMembers.contains(name) + ) { + val fieldId = classCtx.memberFieldIds[name] + val methodId = classCtx.memberMethodIds[name] + if (fieldId != null || methodId != null) { + resolutionSink?.referenceMember(name, pos) + return ImplicitThisMemberRef(name, pos, fieldId, methodId, currentImplicitThisTypeName()) + } + } + captureLocalRef(name, slotLoc, pos)?.let { ref -> + resolutionSink?.reference(name, pos) + return ref + } + val captureOwner = capturePlanStack.lastOrNull()?.captureOwners?.get(name) + if (useFastLocalRefs && + slotLoc.depth == 0 && + captureOwner == null && + currentLocalNames?.contains(name) == true && + currentShadowedLocalNames?.contains(name) != true && + !slotLoc.isDelegated + ) { + resolutionSink?.reference(name, pos) + return FastLocalVarRef(name, pos) + } + if (slotLoc.depth == 0 && captureOwner != null) { + val ref = LocalSlotRef( + name, + slotLoc.slot, + slotLoc.scopeId, + slotLoc.isMutable, + slotLoc.isDelegated, + pos, + strictSlotRefs, + captureOwnerScopeId = captureOwner.scopeId, + captureOwnerSlot = captureOwner.slot + ) + resolutionSink?.reference(name, pos) + return ref + } + val ref = LocalSlotRef( + name, + slotLoc.slot, + slotLoc.scopeId, + slotLoc.isMutable, + slotLoc.isDelegated, + pos, + strictSlotRefs + ) + resolutionSink?.reference(name, pos) + return ref + } + val moduleLoc = if (slotPlanStack.size == 1) lookupSlotLocation(name, includeModule = true) else null + if (moduleLoc != null) { + val moduleDeclaredNames = localNamesStack.firstOrNull() + if (moduleDeclaredNames == null || !moduleDeclaredNames.contains(name)) { + resolveImportBinding(name, pos)?.let { resolved -> + registerImportBinding(name, resolved.binding, pos) + } + } + val ref = LocalSlotRef( + name, + moduleLoc.slot, + moduleLoc.scopeId, + moduleLoc.isMutable, + moduleLoc.isDelegated, + pos, + strictSlotRefs + ) + resolutionSink?.reference(name, pos) + return ref + } + val classCtx = codeContexts.lastOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody + if (classCtx != null && classCtx.declaredMembers.contains(name)) { + resolutionSink?.referenceMember(name, pos) + val ids = resolveMemberIds(name, pos, null) + return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, currentImplicitThisTypeName()) + } + val implicitTypeFromFunc = implicitReceiverTypeForMember(name) + val hasImplicitClassMember = classCtx != null && hasImplicitThisMember(name, classCtx.name) + if (implicitTypeFromFunc == null && !hasImplicitClassMember) { + val modulePlan = moduleSlotPlan() + val moduleEntry = modulePlan?.slots?.get(name) + if (moduleEntry != null) { + val moduleDeclaredNames = localNamesStack.firstOrNull() + if (moduleDeclaredNames == null || !moduleDeclaredNames.contains(name)) { + resolveImportBinding(name, pos)?.let { resolved -> + registerImportBinding(name, resolved.binding, pos) + } + } + val moduleLoc = SlotLocation( + moduleEntry.index, + slotPlanStack.size - 1, + modulePlan.id, + moduleEntry.isMutable, + moduleEntry.isDelegated + ) + captureLocalRef(name, moduleLoc, pos)?.let { ref -> + resolutionSink?.reference(name, pos) + return ref + } + val ref = if (capturePlanStack.isEmpty() && moduleLoc.depth > 0) { + LocalSlotRef( + name, + moduleLoc.slot, + moduleLoc.scopeId, + moduleLoc.isMutable, + moduleLoc.isDelegated, + pos, + strictSlotRefs, + captureOwnerScopeId = moduleLoc.scopeId, + captureOwnerSlot = moduleLoc.slot + ) + } else { + LocalSlotRef( + name, + moduleLoc.slot, + moduleLoc.scopeId, + moduleLoc.isMutable, + moduleLoc.isDelegated, + pos, + strictSlotRefs + ) + } + resolutionSink?.reference(name, pos) + return ref + } + resolveImportBinding(name, pos)?.let { resolved -> + val sourceRecord = resolved.record + if (modulePlan != null && !modulePlan.slots.containsKey(name)) { + val seedSlotIndex = if (resolved.binding.source is ImportBindingSource.Seed) { + seedScope?.getSlotIndexOf(name) + } else { + null + } + val seedSlotFree = seedSlotIndex != null && + modulePlan.slots.values.none { it.index == seedSlotIndex } + if (seedSlotFree) { + declareSlotNameAt( + modulePlan, + name, + seedSlotIndex!!, + sourceRecord.isMutable, + sourceRecord.type == ObjRecord.Type.Delegated + ) + } else { + declareSlotNameIn( + modulePlan, + name, + sourceRecord.isMutable, + sourceRecord.type == ObjRecord.Type.Delegated + ) + } + } + registerImportBinding(name, resolved.binding, pos) + val slot = lookupSlotLocation(name) + if (slot != null) { + captureLocalRef(name, slot, pos)?.let { ref -> + resolutionSink?.reference(name, pos) + return ref + } + val ref = if (capturePlanStack.isEmpty() && slot.depth > 0) { + LocalSlotRef( + name, + slot.slot, + slot.scopeId, + slot.isMutable, + slot.isDelegated, + pos, + strictSlotRefs, + captureOwnerScopeId = slot.scopeId, + captureOwnerSlot = slot.slot + ) + } else { + LocalSlotRef( + name, + slot.slot, + slot.scopeId, + slot.isMutable, + slot.isDelegated, + pos, + strictSlotRefs + ) + } + resolutionSink?.reference(name, pos) + return ref + } + } + } + if (classCtx != null) { + val implicitType = classCtx.name + if (hasImplicitThisMember(name, implicitType)) { + resolutionSink?.referenceMember(name, pos, implicitType) + val ids = resolveImplicitThisMemberIds(name, pos, implicitType) + val preferredType = if (currentImplicitThisTypeName() == null) null else implicitType + return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, preferredType) + } + } + val implicitType = implicitTypeFromFunc + if (implicitType != null) { + resolutionSink?.referenceMember(name, pos, implicitType) + val ids = resolveImplicitThisMemberIds(name, pos, implicitType) + return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, implicitType) + } + if (classCtx != null && classCtx.classScopeMembers.contains(name)) { + resolutionSink?.referenceMember(name, pos, classCtx.name) + return ClassScopeMemberRef(name, pos, classCtx.name) + } + val classContext = codeContexts.any { ctx -> ctx is CodeContext.ClassBody } + if (classContext && extensionNames.contains(name)) { + resolutionSink?.referenceMember(name, pos) + return LocalVarRef(name, pos) + } + resolutionSink?.reference(name, pos) + if (allowUnresolvedRefs) { + return LocalVarRef(name, pos) + } + throw ScriptError(pos, "unresolved name: $name") + } + + private fun isRangeType(type: TypeDecl): Boolean { + val name = when (type) { + is TypeDecl.Simple -> type.name + is TypeDecl.Generic -> type.name + else -> return false + } + return name == "Range" || + name == "IntRange" || + name.endsWith(".Range") || + name.endsWith(".IntRange") + } + var packageName: String? = null class Settings( val miniAstSink: MiniAstSink? = null, + val resolutionSink: ResolutionSink? = null, + val compileBytecode: Boolean = true, + val strictSlotRefs: Boolean = true, + val allowUnresolvedRefs: Boolean = false, + val seedScope: Scope? = null, + val useFastLocalRefs: Boolean = false, ) // Optional sink for mini-AST streaming (null by default, zero overhead when not used) private val miniSink: MiniAstSink? = settings.miniAstSink + private val resolutionSink: ResolutionSink? = settings.resolutionSink + private val compileBytecode: Boolean = settings.compileBytecode + private val seedScope: Scope? = settings.seedScope + private val useFastLocalRefs: Boolean = settings.useFastLocalRefs + private var resolutionScriptDepth = 0 + private val resolutionPredeclared = mutableSetOf() + private data class ImportedModule(val scope: ModuleScope, val pos: Pos) + private data class ImportBindingResolution(val binding: ImportBinding, val record: ObjRecord) + private val importedModules = mutableListOf() + private val importBindings = mutableMapOf() + private val enumEntriesByName = mutableMapOf>() // --- Doc-comment collection state (for immediate preceding declarations) --- private val pendingDocLines = mutableListOf() @@ -129,6 +1152,248 @@ class Compiler( } } + private fun seedResolutionFromScope(scope: Scope, pos: Pos) { + val sink = resolutionSink ?: return + var current: Scope? = scope + while (current != null) { + for ((name, record) in current.objects) { + if (!record.visibility.isPublic) continue + if (!resolutionPredeclared.add(name)) continue + sink.declareSymbol(name, SymbolKind.LOCAL, record.isMutable, pos) + } + current = current.parent + } + } + + private fun seedNameObjClassFromScope(scope: Scope) { + var current: Scope? = scope + while (current != null) { + for ((name, record) in current.objects) { + if (!record.visibility.isPublic) continue + if (nameObjClass.containsKey(name)) continue + when (val value = record.value) { + is ObjClass -> nameObjClass[name] = value + is ObjInstance -> nameObjClass[name] = value.objClass + } + } + current = current.parent + } + } + + private fun resolveImportBinding(name: String, pos: Pos): ImportBindingResolution? { + val seedRecord = findSeedScopeRecord(name)?.takeIf { it.visibility.isPublic } + val rootRecord = importManager.rootScope.objects[name]?.takeIf { it.visibility.isPublic } + val moduleMatches = LinkedHashMap>() + for (module in importedModules.asReversed()) { + val found = LinkedHashMap>() + collectModuleRecordMatches(module.scope, name, mutableSetOf(), found) + for ((pkg, pair) in found) { + if (!moduleMatches.containsKey(pkg)) { + moduleMatches[pkg] = ImportedModule(pair.first, module.pos) to pair.second + } + } + } + if (seedRecord != null) { + val value = seedRecord.value + if (!nameObjClass.containsKey(name)) { + when (value) { + is ObjClass -> nameObjClass[name] = value + is ObjInstance -> nameObjClass[name] = value.objClass + } + } + return ImportBindingResolution(ImportBinding(name, ImportBindingSource.Seed), seedRecord) + } + if (rootRecord != null) { + val value = rootRecord.value + if (!nameObjClass.containsKey(name)) { + when (value) { + is ObjClass -> nameObjClass[name] = value + is ObjInstance -> nameObjClass[name] = value.objClass + } + } + return ImportBindingResolution(ImportBinding(name, ImportBindingSource.Root), rootRecord) + } + if (moduleMatches.isEmpty()) return null + if (moduleMatches.size > 1) { + val moduleNames = moduleMatches.keys.toList() + throw ScriptError(pos, "symbol $name is ambiguous between imports: ${moduleNames.joinToString(", ")}") + } + val (module, record) = moduleMatches.values.first() + val binding = ImportBinding(name, ImportBindingSource.Module(module.scope.packageName, module.pos)) + val value = record.value + if (!nameObjClass.containsKey(name)) { + when (value) { + is ObjClass -> nameObjClass[name] = value + is ObjInstance -> nameObjClass[name] = value.objClass + } + } + return ImportBindingResolution(binding, record) + } + + private fun collectModuleRecordMatches( + scope: ModuleScope, + name: String, + visited: MutableSet, + out: MutableMap> + ) { + if (!visited.add(scope.packageName)) return + val record = scope.objects[name] + if (record != null && record.visibility.isPublic) { + if (!out.containsKey(scope.packageName)) { + out[scope.packageName] = scope to record + } + } + for (child in scope.importedModules) { + collectModuleRecordMatches(child, name, visited, out) + } + } + + private fun registerImportBinding(name: String, binding: ImportBinding, pos: Pos) { + val existing = importBindings[name] ?: run { + importBindings[name] = binding + scopeSeedNames.add(name) + return + } + if (!sameImportBinding(existing, binding)) { + throw ScriptError(pos, "symbol $name resolves to multiple imports") + } + } + + private fun sameImportBinding(left: ImportBinding, right: ImportBinding): Boolean { + if (left.symbol != right.symbol) return false + val leftSrc = left.source + val rightSrc = right.source + return when (leftSrc) { + is ImportBindingSource.Module -> { + rightSrc is ImportBindingSource.Module && leftSrc.name == rightSrc.name + } + ImportBindingSource.Root -> rightSrc is ImportBindingSource.Root + ImportBindingSource.Seed -> rightSrc is ImportBindingSource.Seed + } + } + + private fun findSeedScopeRecord(name: String): ObjRecord? { + var current = seedScope + var hops = 0 + while (current != null && hops++ < 1024) { + current.objects[name]?.let { return it } + current = current.parent + } + return null + } + + private fun shouldSeedDefaultStdlib(): Boolean { + if (seedScope != null) return false + if (importManager !== Script.defaultImportManager) return false + val sourceName = cc.tokens.firstOrNull()?.pos?.source?.fileName + return sourceName != "lyng.stdlib" + } + + private fun looksLikeExtensionReceiver(): Boolean { + val saved = cc.savePos() + try { + if (cc.peekNextNonWhitespace().type != Token.Type.ID) return false + cc.nextNonWhitespace() + // consume qualified name segments + while (cc.peekNextNonWhitespace().type == Token.Type.DOT) { + val dotPos = cc.savePos() + cc.nextNonWhitespace() + if (cc.peekNextNonWhitespace().type != Token.Type.ID) { + cc.restorePos(dotPos) + break + } + cc.nextNonWhitespace() + val afterSegment = cc.peekNextNonWhitespace() + if (afterSegment.type != Token.Type.DOT && + afterSegment.type != Token.Type.LT && + afterSegment.type != Token.Type.QUESTION && + afterSegment.type != Token.Type.IFNULLASSIGN + ) { + cc.restorePos(dotPos) + break + } + } + // optional generic arguments + if (cc.peekNextNonWhitespace().type == Token.Type.LT) { + var depth = 0 + while (true) { + val tok = cc.nextNonWhitespace() + when (tok.type) { + Token.Type.LT -> depth += 1 + Token.Type.GT -> { + depth -= 1 + if (depth <= 0) break + } + Token.Type.SHR -> { + depth -= 2 + if (depth <= 0) break + } + Token.Type.EOF -> return false + else -> {} + } + } + } + // nullable suffix + if (cc.peekNextNonWhitespace().type == Token.Type.QUESTION || + cc.peekNextNonWhitespace().type == Token.Type.IFNULLASSIGN + ) { + cc.nextNonWhitespace() + } + val dotTok = cc.peekNextNonWhitespace() + if (dotTok.type != Token.Type.DOT) return false + val savedDot = cc.savePos() + cc.nextNonWhitespace() + val nameTok = cc.peekNextNonWhitespace() + cc.restorePos(savedDot) + return nameTok.type == Token.Type.ID + } finally { + cc.restorePos(saved) + } + } + + private fun shouldImplicitTypeVar(name: String, explicit: Set): Boolean { + if (explicit.contains(name)) return true + if (name.contains('.')) return false + if (resolveClassByName(name) != null) return false + if (resolveTypeDeclObjClass(TypeDecl.Simple(name, false)) != null) return false + return name.length == 1 || name in setOf("T", "R", "E", "K", "V") + } + + private fun normalizeReceiverTypeDecl( + receiver: TypeDecl?, + explicitTypeParams: Set + ): Pair> { + if (receiver == null) return null to emptySet() + val implicit = mutableSetOf() + fun transform(decl: TypeDecl): TypeDecl = when (decl) { + is TypeDecl.Simple -> { + if (shouldImplicitTypeVar(decl.name, explicitTypeParams)) { + if (!explicitTypeParams.contains(decl.name)) implicit += decl.name + TypeDecl.TypeVar(decl.name, decl.isNullable) + } else decl + } + is TypeDecl.TypeVar -> { + if (!explicitTypeParams.contains(decl.name)) implicit += decl.name + decl + } + is TypeDecl.Generic -> TypeDecl.Generic( + decl.name, + decl.args.map { transform(it) }, + decl.isNullable + ) + is TypeDecl.Function -> TypeDecl.Function( + receiver = decl.receiver?.let { transform(it) }, + params = decl.params.map { transform(it) }, + returnType = transform(decl.returnType), + nullable = decl.isNullable + ) + is TypeDecl.Union -> TypeDecl.Union(decl.options.map { transform(it) }, decl.isNullable) + is TypeDecl.Intersection -> TypeDecl.Intersection(decl.options.map { transform(it) }, decl.isNullable) + else -> decl + } + return transform(receiver) to implicit + } + private var anonCounter = 0 private fun generateAnonName(pos: Pos): String { return "${"$"}${"Anon"}_${pos.line+1}_${pos.column}_${++anonCounter}" @@ -179,6 +1444,18 @@ class Compiler( private val initStack = mutableListOf>() + private data class CompileClassInfo( + val name: String, + val fieldIds: Map, + val methodIds: Map, + val nextFieldId: Int, + val nextMethodId: Int, + val baseNames: List + ) + + private val compileClassInfos = mutableMapOf() + private val compileClassStubs = mutableMapOf() + val currentInitScope: MutableList get() = initStack.lastOrNull() ?: cc.syntaxError("no initialization scope exists here") @@ -194,6 +1471,7 @@ class Compiler( private suspend fun inCodeContext(context: CodeContext, f: suspend () -> T): T { codeContexts.add(context) + pushGenericFunctionScope() try { val res = f() if (context is CodeContext.ClassBody) { @@ -204,6 +1482,7 @@ class Compiler( } return res } finally { + popGenericFunctionScope() codeContexts.removeLast() } } @@ -211,12 +1490,45 @@ class Compiler( private suspend fun parseScript(): Script { val statements = mutableListOf() val start = cc.currentPos() + val atTopLevel = resolutionSink != null && resolutionScriptDepth == 0 + if (atTopLevel) { + resolutionSink?.enterScope(ScopeKind.MODULE, start, null) + seedScope?.let { seedResolutionFromScope(it, start) } + seedResolutionFromScope(importManager.rootScope, start) + } + resolutionScriptDepth++ // Track locals at script level for fast local refs - return withLocalNames(emptySet()) { - // package level declarations - // Notify sink about script start - miniSink?.onScriptStart(start) - do { + val needsSlotPlan = slotPlanStack.isEmpty() + if (needsSlotPlan) { + slotPlanStack.add(SlotPlan(mutableMapOf(), 0, nextScopeId++)) + seedScope?.let { scope -> + if (scope !is ModuleScope) { + seedSlotPlanFromSeedScope(scope) + } + } + val plan = slotPlanStack.last() + if (!plan.slots.containsKey("__PACKAGE__")) { + declareSlotNameIn(plan, "__PACKAGE__", isMutable = false, isDelegated = false) + } + if (!plan.slots.containsKey("$~")) { + declareSlotNameIn(plan, "$~", isMutable = true, isDelegated = false) + } + seedScope?.let { seedNameObjClassFromScope(it) } + seedNameObjClassFromScope(importManager.rootScope) + if (shouldSeedDefaultStdlib()) { + val stdlib = importManager.prepareImport(start, "lyng.stdlib", null) + seedResolutionFromScope(stdlib, start) + seedNameObjClassFromScope(stdlib) + importedModules.add(ImportedModule(stdlib, start)) + } + predeclareTopLevelSymbols() + } + return try { + withLocalNames(emptySet()) { + // package level declarations + // Notify sink about script start + miniSink?.onScriptStart(start) + do { val t = cc.current() if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { when (t.type) { @@ -276,13 +1588,8 @@ class Compiler( } } val module = importManager.prepareImport(pos, name, null) - statements += object : Statement() { - override val pos: Pos = pos - override suspend fun execute(scope: Scope): Obj { - module.importInto(scope, null) - return ObjVoid - } - } + importedModules.add(ImportedModule(module, pos)) + seedResolutionFromScope(module, pos) continue } } @@ -310,14 +1617,66 @@ class Compiler( break } - } while (true) - Script(start, statements) - }.also { - // Best-effort script end notification (use current position) - miniSink?.onScriptEnd( - cc.currentPos(), - MiniScript(MiniRange(start, cc.currentPos())) - ) + } while (true) + val modulePlan = if (needsSlotPlan) slotPlanIndices(slotPlanStack.last()) else emptyMap() + val forcedLocalInfo = if (useScopeSlots) emptyMap() else moduleForcedLocalSlotInfo() + val forcedLocalScopeId = if (useScopeSlots) null else moduleSlotPlan()?.id + val allowedScopeNames = if (useScopeSlots) modulePlan.keys else null + val scopeSlotNameSet = if (useScopeSlots) scopeSeedNames else null + val moduleScopeId = if (useScopeSlots) null else moduleSlotPlan()?.id + val isModuleScript = codeContexts.lastOrNull() is CodeContext.Module && resolutionScriptDepth == 1 + val wrapScriptBytecode = compileBytecode && isModuleScript + val (finalStatements, moduleBytecode) = if (wrapScriptBytecode) { + val unwrapped = statements.map { unwrapBytecodeDeep(it) } + val block = InlineBlockStatement(unwrapped, start) + val bytecodeStmt = BytecodeStatement.wrap( + block, + "