mt-lang 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 990a1e0ede945713a31152caccc4a7de798dc6ead6078d46d5a376f19054a52a
4
- data.tar.gz: 97d7877587ca50ab673212d1d0d80d6d60ad8f97a74c439965feb32c2f5a4e37
3
+ metadata.gz: 726a973cae08da6d7927931a6fa51adc8106fd483709dede501b7e551e1d9103
4
+ data.tar.gz: 666501d1a75719851869d1467d7498e84c2b4fd18da6d52a41912343d2f52f43
5
5
  SHA512:
6
- metadata.gz: c04b4d611724c8b4f42ae14929b3d8d98b4aa4da5157f7c3ee765afb6356be643b6ada8c96ed45a8e61103e71792c7855c25d87f65865b9609cdb0cc85544507
7
- data.tar.gz: 82416c2665d49928f0d6dd5e9b8f225177f9df0f782e973339545a6375cc673e082bccc3bbb6126cbdc50875dc6fa29880b5e23b8e3ccd458934b32f5d2f6718
6
+ metadata.gz: 618b5705f9b727ce7d07b6ccf6576aa92d0380d7f4609393790fb8c64df1a3c7bd409151eabc800d95ca01d6636c8e26d43840758db90502ccf2757ad6800190
7
+ data.tar.gz: e9a6840730d61fee0b57c3339779169a31e91d84626230cc353c1e9b31a1e748b58a0b5c205d904bbb32ba0951bfd4874df3374c378a62c8a6e8277bc1fccffe
data/README.md CHANGED
@@ -364,6 +364,8 @@ Method kinds:
364
364
  - `function` -> value receiver
365
365
  - `editable function` -> editable receiver
366
366
  - `static function` -> no receiver
367
+ - `const function` -> value receiver, compile-time-evaluable
368
+ - `static const function` -> no receiver, compile-time-evaluable
367
369
 
368
370
  Methods may appear inside a struct body (desugared to an `extending` block) or in a separate
369
371
  `extending` declaration:
@@ -424,6 +426,8 @@ const RESULT: int = square(5) # folded to 25 at compile time
424
426
 
425
427
  `const function` also generates a normal runtime function, callable from ordinary runtime code.
426
428
 
429
+ `const` applies to methods as well: `const function` (value receiver) and `static const function` (no receiver) methods fold when called from a compile-time context and also generate normal runtime functions. `editable const function`, `async const function`, and `const` on interface methods are rejected.
430
+
427
431
  External functions:
428
432
 
429
433
  ```mt
data/docs/index.html CHANGED
@@ -556,6 +556,14 @@ function make_buffer[N: int]() -> str_buffer[N]:
556
556
  var buf: str_buffer[N]
557
557
  return buf
558
558
 
559
+ ## ── inline if with a compile-time type comparison ───────────────────
560
+ function size_label[T]() -> str:
561
+ inline if T == int:
562
+ return "32-bit"
563
+ inline if T == float:
564
+ return "float"
565
+ return "other"
566
+
559
567
  ## ── Error handling with Result[T, E] ────────────────────────────────
560
568
  enum LoadError: ubyte
561
569
  not_found = 1
@@ -575,6 +583,12 @@ function load_pair() -> Result[bool, LoadError]:
575
583
  io.print_line(f"Loaded #{first.name} and #{second.name}")
576
584
  return Result[bool, LoadError].success(value = true)
577
585
 
586
+ ## ── Foreign FFI: raw external + foreign projection ──────────────────
587
+ external function atoi(input: cstr) -> int
588
+
589
+ ## foreign function projects a raw ABI call into ordinary Milk Tea types
590
+ foreign function parse_int_foreign(input: str as cstr) -> int = atoi
591
+
578
592
  ## ── Events (fixed-capacity pub/sub, zero heap during dispatch) ──────
579
593
  struct DamageEvent:
580
594
  target_id: EntityId
@@ -601,6 +615,19 @@ async function load_scores() -> int:
601
615
  ## ── Compile-time constants for inline if ─────────────────────────────
602
616
  const DEBUG: bool = false
603
617
 
618
+ ## ── when: compile-time conditional (only the chosen branch is emitted) ──
619
+ enum Backend: ubyte
620
+ gl = 1
621
+ vulkan = 2
622
+ const TARGET_BACKEND: Backend = Backend.gl
623
+
624
+ function backend_label() -> str:
625
+ when TARGET_BACKEND:
626
+ Backend.gl:
627
+ return "OpenGL"
628
+ Backend.vulkan:
629
+ return "Vulkan"
630
+
604
631
  ## ── Main entry point ────────────────────────────────────────────────
605
632
  function main() -> int:
606
633
  ## Local declarations: let (immutable), var (mutable)
@@ -727,12 +754,22 @@ function main() -> int:
727
754
  Action.idle:
728
755
  pass
729
756
 
757
+ ## is: variant arm membership test
758
+ if action is Action.attack:
759
+ io.print_line("action is an attack")
760
+
730
761
  ## match expression
731
762
  let label = match player.level:
732
763
  1: "beginner"
733
764
  2: "novice"
734
765
  _: "advanced"
735
766
 
767
+ ## Struct equality: field-wise ==/!=
768
+ let stats_a = Entity.Stats(attack = 10, defense = 5, speed = 1.0)
769
+ let stats_b = Entity.Stats(attack = 10, defense = 5, speed = 1.0)
770
+ if stats_a == stats_b:
771
+ io.print_line("stats equal")
772
+
736
773
  ## Explicit cast: T<-value
737
774
  let raw_kind = ubyte<-player.entity.kind
738
775
  let ratio = float<-score
@@ -795,6 +832,12 @@ function main() -> int:
795
832
  inline if DEBUG:
796
833
  io.print_line("debug mode")
797
834
 
835
+ ## Foreign FFI, when, and inline-if type dispatch
836
+ let parsed_int = parse_int_foreign("42")
837
+ io.print_line(f"FFI parsed: #{parsed_int}")
838
+ io.print_line(f"Backend: #{backend_label()}")
839
+ io.print_line(f"int label: #{size_label[int]()}")
840
+
798
841
  ## ── Concurrency ─────────────────────────────────────────────────────
799
842
  ## parallel for: data-parallel loop dispatched across CPU cores
800
843
  var positions = array[float, 4](0.0, 1.0, 2.0, 3.0)
@@ -829,7 +872,7 @@ function main() -> int:
829
872
  </div>
830
873
 
831
874
  <div class="callout tip">
832
- <strong>Tip:</strong> This example covers imports, constants, <code>const function</code>, type aliases, enums, flags, structs (nested, attributed), variants, interfaces, <code>extending</code> methods, generics with constraints, value parameter generics, <code>Result</code> error handling with <code>?</code> and guard binding, events, closures (<code>proc</code>), <code>async</code>/<code>await</code>, concurrency (<code>parallel for</code>, <code>parallel:</code>, <code>detach</code> + <code>gather</code>, <code>atomic[T]</code>), native vector types, arrays, spans, collections, <code>for</code>/<code>while</code>/<code>match</code>/<code>if</code>, variant membership test (<code>is</code>), parallel <code>for</code>, <code>defer</code>, <code>unsafe</code>, <code>ref[T]</code>, <code>own[T]</code>, <code>dyn[Interface]</code>, format strings, heredocs, <code>str_buffer</code>, compile-time reflection, <code>inline for</code>/<code>inline if</code>, <code>static_assert</code>, and explicit casts. See the sections below for full details on each feature.
875
+ <strong>Tip:</strong> This example covers imports, constants, <code>const function</code>, type aliases, enums, flags, structs (nested, attributed), variants, interfaces, foreign functions (<code>external</code> / <code>foreign function</code>), <code>extending</code> methods, generics with constraints, value parameter generics, <code>Result</code> error handling with <code>?</code> and guard binding, events, closures (<code>proc</code>), <code>async</code>/<code>await</code>, concurrency (<code>parallel for</code>, <code>parallel:</code>, <code>detach</code> + <code>gather</code>, <code>atomic[T]</code>), native vector types, arrays, spans, collections, <code>for</code>/<code>while</code>/<code>match</code>/<code>if</code>, variant membership test (<code>is</code>), parallel <code>for</code>, <code>defer</code>, <code>unsafe</code>, <code>ref[T]</code>, <code>own[T]</code>, <code>dyn[Interface]</code>, format strings, heredocs, <code>str_buffer</code>, compile-time reflection, <code>when</code>, <code>inline for</code>/<code>inline if</code> (including type comparison), struct <code>==</code>/<code>!=</code>, <code>static_assert</code>, and explicit casts. See the sections below for full details on each feature.
833
876
  </div>
834
877
  </section>
835
878
 
@@ -1221,7 +1264,7 @@ struct NPC implements Damageable:
1221
1264
  </div>
1222
1265
 
1223
1266
  <h3>Methods (extending)</h3>
1224
- <p>Three kinds: <code>function</code> (value receiver), <code>editable function</code> (editable receiver), <code>static function</code> (no receiver).</p>
1267
+ <p>Method kinds: <code>function</code> (value receiver), <code>editable function</code> (editable receiver), <code>static function</code> (no receiver), plus compile-time-evaluable <code>const function</code> and <code>static const function</code>. <code>editable const function</code>, <code>async const function</code>, and <code>const</code> on interface methods are rejected.</p>
1225
1268
  <div class="code-wrap">
1226
1269
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1227
1270
  <pre><code>extending Counter:
@@ -1394,6 +1437,7 @@ function say_hello(name: str):
1394
1437
 
1395
1438
  <h3>const function</h3>
1396
1439
  <p>Evaluable at compile time. Generates both a compile-time constant-folding path and a normal runtime function. Recursive calls between <code>const</code> functions are supported. Use <code>const function</code> for reusable compile-time logic; use a block-bodied <code>const X -&gt; T: ...</code> for one-shot computed constants.</p>
1440
+ <p><code>const</code> also applies to methods: <code>const function</code> (value receiver) and <code>static const function</code> (no receiver) fold when called from a compile-time context. <code>editable const function</code>, <code>async const function</code>, and <code>const</code> on interface methods are rejected.</p>
1397
1441
  <div class="code-wrap">
1398
1442
  <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1399
1443
  <pre><code>const function square(x: int) -> int:
@@ -1656,6 +1700,31 @@ unsafe:
1656
1700
  <section id="compile-time">
1657
1701
  <h2>Compile-Time Control Flow</h2>
1658
1702
 
1703
+ <h3>Block-bodied <code>const</code> and <code>const function</code></h3>
1704
+ <p>Block-bodied <code>const</code> initializers and <code>const function</code> bodies are evaluated at compile time, and the result is folded into the emitted C. Supported surface: literals (character literals fold to their byte value), other <code>const</code> values, arithmetic, <code>str</code> concatenation, <code>if</code>/<code>while</code>/<code>for</code> with <code>break</code> and <code>continue</code>, <code>match</code> expressions, index and range access, struct member access, <code>let</code>/<code>var</code> with destructuring and <code>else:</code> / <code>else as error:</code> guards, assignment (including compound), numeric prefix casts, calls to other const functions and const methods, and the reflection builtins.</p>
1705
+ <div class="code-wrap">
1706
+ <button class="copy-btn" onclick="copyCode(this)">Copy</button>
1707
+ <pre><code>## str concatenation folds to a literal
1708
+ const GREET: str = "hello" + " " + "world"
1709
+
1710
+ ## match expressions fold
1711
+ const LABEL -&gt; str:
1712
+ return match 2:
1713
+ 1: "one"
1714
+ 2: "two"
1715
+ _: "other"
1716
+
1717
+ ## const methods fold too
1718
+ struct Rect:
1719
+ w: int
1720
+ h: int
1721
+ const function area() -&gt; int:
1722
+ return this.w * this.h
1723
+
1724
+ const R: Rect = Rect(w = 10, h = 20)
1725
+ const AREA: int = R.area() ## folded to 200 at compile time</code></pre>
1726
+ </div>
1727
+
1659
1728
  <h3>when</h3>
1660
1729
  <p>Evaluates discriminant at compile time; only the chosen branch is type-checked and emitted. <code>when</code> may also appear at module level to conditionally include declarations, imports, or type definitions.</p>
1661
1730
  <div class="code-wrap">
@@ -79,7 +79,7 @@ The intended reductions are deliberate:
79
79
 
80
80
  If a new feature introduces a second ordinary way to express the same concept, the language should delete one of them instead of documenting both.
81
81
 
82
- The compile-time evaluation surface is described in [Compile-Time Evaluation](compile-time.md).
82
+ The compile-time evaluation surface is described in the [language manual](language-manual.md): block-bodied `const` and `const function` limits in §3.2 and §3.7a, and `when` plus the `inline` statements in §4.7–§4.11.
83
83
 
84
84
  ## Overall shape
85
85
 
@@ -716,7 +716,7 @@ type FileHandle = ptr[libc.FILE]
716
716
 
717
717
  ### Generics
718
718
 
719
- Generics are useful, but they must stay boring. The compile-time surface is documented in [Compile-Time Evaluation](compile-time.md); generic bodies participate in that surface by using `when`, `inline for`, `inline while`, `inline match`, `inline if`, `type`-returning functions, `const function`, and block-bodied `const` initializers (`const X -> T: ...`) at the lexical positions where the compile-time rules allow them.
719
+ Generics are useful, but they must stay boring. The compile-time surface is documented in the [language manual](language-manual.md); generic bodies participate in that surface by using `when`, `inline for`, `inline while`, `inline match`, `inline if`, `type`-returning functions, `const function`, and block-bodied `const` initializers (`const X -> T: ...`) at the lexical positions where the compile-time rules allow them.
720
720
 
721
721
  Allowed in v1:
722
722
 
@@ -208,7 +208,7 @@ const NEXT_POW2 -> int:
208
208
  return n
209
209
  ```
210
210
 
211
- The block body is evaluated at compile time. Allowed inside the block: literals, names of other `const` values, arithmetic, control flow (`if`/`else if`/`else`, `while`, `for`), `let` and `var` declarations, calls to other compile-time functions, and calls to whitelisted builtins (`size_of`, `align_of`, `offset_of`, `fields_of`, `members_of`, `attributes_of`).
211
+ The block body is evaluated at compile time. Allowed inside the block: literals (character literals fold to their byte value), names of other `const` values, arithmetic, `str` concatenation (`+`), control flow (`if`/`else if`/`else`, `while`, `for`, with `break` and `continue`), `match` expressions, index and range access (`arr[i]`, `s[i]`, `arr[start..stop]`, `s[start..stop]`), struct member access, `let` and `var` declarations (including tuple/struct destructuring and `else:` / `else as error:` guard forms), assignment (including compound assignment to block-local struct fields and array elements), numeric prefix casts (`T<-value`), calls to other compile-time functions and const methods, and calls to whitelisted builtins (`size_of`, `align_of`, `offset_of`, `fields_of`, `members_of`, `attributes_of`).
212
212
 
213
213
  Rules:
214
214
 
@@ -524,6 +524,8 @@ Kinds:
524
524
  - `function` (value receiver)
525
525
  - `editable function` (editable receiver)
526
526
  - `static function` (no receiver)
527
+ - `const function` (value receiver, compile-time-evaluable)
528
+ - `static const function` (no receiver, compile-time-evaluable)
527
529
 
528
530
  Names such as `init` and `default` are ordinary static functions. There is no constructor keyword or hidden initializer syntax.
529
531
 
@@ -531,6 +533,9 @@ Method capabilities:
531
533
 
532
534
  - async methods are supported
533
535
  - generic methods are supported
536
+ - `const function` and `static const function` methods follow the same compile-time body rules as a block-bodied `const` (§3.2). Called from a compile-time context, the call is constant-folded; they also generate normal runtime functions.
537
+ - `editable const function` and `async const function` are rejected.
538
+ - `const` is not allowed on interface methods.
534
539
 
535
540
  ### 3.7 Functions
536
541
 
@@ -591,10 +596,11 @@ const SQUARE_5: int = square(5) # folded to 25 at compile time
591
596
 
592
597
  Rules:
593
598
 
594
- - The body must be evaluable at compile time (literals, `const` values, arithmetic, `if`/`else`, `while`, `for`, `let`/`var`, calls to other `const` functions, and whitelisted builtins).
599
+ - The body follows the same compile-time rules as a block-bodied `const` (§3.2).
595
600
  - Generates a normal runtime function as well — callable from ordinary runtime code.
596
601
  - Called from `const` initializers, `when` discriminants, `inline for` bodies, and other compile-time contexts.
597
602
  - Recursive calls between `const` functions are supported.
603
+ - `const` applies to methods as well: `const function` (value receiver) and `static const function` (no receiver) methods fold when called from compile-time contexts (§3.6).
598
604
 
599
605
  ### 3.8 External functions
600
606
 
data/lib/milk_tea/base.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module MilkTea
6
- VERSION = "0.4.0"
6
+ VERSION = "0.4.2"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -116,7 +116,7 @@ module MilkTea
116
116
  func && func.respond_to?(:ast) && func.ast.respond_to?(:const) && func.ast.const
117
117
  end
118
118
 
119
- def comptime_const_method_body(binding, arguments, scopes:, receiver_value:)
119
+ def comptime_const_method_body(binding, arguments, scopes:, receiver_value:, arg_evaluator: nil)
120
120
  method = binding.ast
121
121
  return nil unless method.respond_to?(:body) && method.body
122
122
  return nil if binding.type_params.any?
@@ -130,7 +130,11 @@ module MilkTea
130
130
  end
131
131
 
132
132
  method.params.each_with_index do |param, idx|
133
- arg_value = comptime_eval(arguments[idx].value, scopes)
133
+ arg_value = if arg_evaluator
134
+ arg_evaluator.call(arguments[idx].value)
135
+ else
136
+ comptime_eval(arguments[idx].value, scopes)
137
+ end
134
138
  return nil unless arg_value
135
139
 
136
140
  initial_vars[param.name] = arg_value
@@ -713,20 +713,31 @@ module MilkTea
713
713
  return unless @checker.respond_to?(:comptime_method_binding_for_receiver)
714
714
  return unless @checker.respond_to?(:comptime_const_method_body)
715
715
 
716
+ arg_evaluator = ->(expr) { evaluate_expression(expr, scopes:) }
717
+
716
718
  receiver = call_expr.callee.receiver
717
- return unless receiver.is_a?(AST::Identifier)
718
- return unless @variables.key?(receiver.name)
719
+ if receiver.is_a?(AST::Identifier) && @variables.key?(receiver.name)
720
+ receiver_value = @variables[receiver.name]
721
+ return nil if receiver_value.nil? || receiver_value.is_a?(Types::Base)
722
+
723
+ receiver_type = @variable_types[receiver.name]
724
+ return nil unless receiver_type
725
+
726
+ binding = @checker.comptime_method_binding_for_receiver(receiver_type, call_expr.callee.member)
727
+ return nil unless binding
719
728
 
720
- receiver_value = @variables[receiver.name]
721
- return nil if receiver_value.nil? || receiver_value.is_a?(Types::Base)
729
+ return @checker.comptime_const_method_body(binding, call_expr.arguments, scopes:, receiver_value:, arg_evaluator:)
730
+ end
731
+
732
+ return nil unless @checker.respond_to?(:resolve_type_expression)
722
733
 
723
- receiver_type = @variable_types[receiver.name]
734
+ receiver_type = @checker.resolve_type_expression(receiver)
724
735
  return nil unless receiver_type
725
736
 
726
737
  binding = @checker.comptime_method_binding_for_receiver(receiver_type, call_expr.callee.member)
727
738
  return nil unless binding
728
739
 
729
- @checker.comptime_const_method_body(binding, call_expr.arguments, scopes:, receiver_value:)
740
+ @checker.comptime_const_method_body(binding, call_expr.arguments, scopes:, receiver_value: nil, arg_evaluator:)
730
741
  end
731
742
 
732
743
  def try_const_function_call(call_expr, scopes:)
@@ -774,9 +785,12 @@ module MilkTea
774
785
  elsif call_expr.callee.respond_to?(:name)
775
786
  call_expr.callee.name
776
787
  end
777
- return unless callee_name
778
788
 
779
- type = types[callee_name]
789
+ type = if callee_name
790
+ types[callee_name]
791
+ elsif call_expr.callee.is_a?(AST::MemberAccess) && @checker.respond_to?(:resolve_type_expression)
792
+ @checker.resolve_type_expression(call_expr.callee)
793
+ end
780
794
  return unless type.is_a?(Types::Struct)
781
795
 
782
796
  fields = {}
@@ -1875,6 +1875,10 @@ module MilkTea
1875
1875
  @lowerer.resolve_type_ref(type_ref)
1876
1876
  end
1877
1877
 
1878
+ def resolve_type_expression(expression)
1879
+ @lowerer.resolve_type_expression(expression)
1880
+ end
1881
+
1878
1882
  def comptime_struct_field_names(type_name)
1879
1883
  @lowerer.comptime_struct_field_names(type_name)
1880
1884
  end
@@ -1883,8 +1887,8 @@ module MilkTea
1883
1887
  @lowerer.comptime_method_binding_for_receiver(receiver_type, member)
1884
1888
  end
1885
1889
 
1886
- def comptime_const_method_body(binding, arguments, scopes: nil, receiver_value: nil)
1887
- @lowerer.comptime_const_method_body(binding, arguments, scopes:, receiver_value:)
1890
+ def comptime_const_method_body(binding, arguments, scopes: nil, receiver_value: nil, arg_evaluator: nil)
1891
+ @lowerer.comptime_const_method_body(binding, arguments, scopes:, receiver_value:, arg_evaluator:)
1888
1892
  end
1889
1893
 
1890
1894
  def comptime_expression_type(expression, scopes: nil)
@@ -190,7 +190,8 @@ module MilkTea
190
190
  "return type mismatch: expected #{return_type}, got #{value_type}",
191
191
  expression: statement.value,
192
192
  contextual_int_to_float: contextual_int_to_float_target?(return_type),
193
- line: statement.line,
193
+ line: (statement.line unless statement.value),
194
+ column: (statement.column unless statement.value),
194
195
  )
195
196
  when AST::DeferStmt
196
197
  with_loop_barrier do
@@ -424,6 +424,17 @@ module MilkTea
424
424
  receiver_value = comptime_member_receiver_value(expression.callee.receiver, scopes)
425
425
  return comptime_const_method_body(method_binding, expression.arguments, scopes:, receiver_value:)
426
426
  end
427
+
428
+ if (struct_type = resolve_type_expression(expression.callee)) && struct_type.is_a?(Types::Struct)
429
+ fields = {}
430
+ expression.arguments.each do |argument|
431
+ val = evaluate_compile_time_const_value(argument.value, scopes:)
432
+ return nil unless val
433
+
434
+ fields[argument.name] = val
435
+ end
436
+ return fields
437
+ end
427
438
  when AST::Identifier
428
439
  if (struct_type = @ctx.types[expression.callee.name]) && struct_type.is_a?(Types::Struct)
429
440
  fields = {}
@@ -459,8 +459,8 @@ module MilkTea
459
459
  return nil if prefix.empty?
460
460
  return nil unless @current_completion_trigger_kind == TRIGGER_KIND_INCOMPLETE
461
461
 
462
- content = @workspace.get_content(uri)
463
- line_prefix = (content.split("\n", -1)[lsp_line] || '')[0...lsp_char]
462
+ lines = @workspace.document_lines(uri)
463
+ line_prefix = (lines[lsp_line] || '')[0...lsp_char]
464
464
  cached = @completion_session_cache[[uri, lsp_line]]
465
465
  return nil unless cached
466
466
  return nil unless line_prefix.start_with?(cached[:line_prefix])
@@ -473,8 +473,8 @@ module MilkTea
473
473
  def store_completion_session(uri, lsp_line, lsp_char, prefix, response)
474
474
  return if prefix.empty?
475
475
 
476
- content = @workspace.get_content(uri)
477
- line_prefix = (content.split("\n", -1)[lsp_line] || '')[0...lsp_char]
476
+ lines = @workspace.document_lines(uri)
477
+ line_prefix = (lines[lsp_line] || '')[0...lsp_char]
478
478
  key = [uri, lsp_line]
479
479
  @completion_session_cache[key] = {
480
480
  line_prefix: line_prefix,
@@ -875,8 +875,7 @@ module MilkTea
875
875
  end
876
876
 
877
877
  def import_completions(uri, lsp_line, lsp_char)
878
- content = @workspace.get_content(uri)
879
- lines = content.split("\n", -1)
878
+ lines = @workspace.document_lines(uri)
880
879
  line = lines[lsp_line] || ''
881
880
  stripped = line.lstrip
882
881
  return nil unless stripped.start_with?('import ')
@@ -936,9 +935,7 @@ module MilkTea
936
935
  end
937
936
 
938
937
  def attribute_completions(facts, uri, line, char)
939
- content = @workspace.get_content(uri)
940
- return nil unless content
941
- lines = content.split("\n", -1)
938
+ lines = @workspace.document_lines(uri)
942
939
  line_text = lines[line] || ''
943
940
  return nil if line_text.empty?
944
941
 
@@ -980,9 +977,7 @@ module MilkTea
980
977
  def format_string_completions(facts, uri, line, char)
981
978
  return nil unless facts
982
979
 
983
- content = @workspace.get_content(uri)
984
- return nil unless content
985
- lines = content.split("\n", -1)
980
+ lines = @workspace.document_lines(uri)
986
981
  line_text = lines[line] || ''
987
982
  return nil if line_text.empty?
988
983
 
@@ -1045,9 +1040,7 @@ module MilkTea
1045
1040
  def named_argument_completions(facts, uri, line, char)
1046
1041
  return nil unless facts
1047
1042
 
1048
- content = @workspace.get_content(uri)
1049
- return nil unless content
1050
- lines = content.split("\n", -1)
1043
+ lines = @workspace.document_lines(uri)
1051
1044
  line_text = lines[line] || ''
1052
1045
  return nil if line_text.empty?
1053
1046
 
@@ -1109,9 +1102,7 @@ module MilkTea
1109
1102
  def specialization_completions(facts, _uri, line, char)
1110
1103
  return nil unless facts
1111
1104
 
1112
- content = @workspace.get_content(_uri)
1113
- return nil unless content
1114
- lines = content.split("\n", -1)
1105
+ lines = @workspace.document_lines(_uri)
1115
1106
  line_text = lines[line] || ''
1116
1107
  return nil if line_text.empty?
1117
1108
 
@@ -295,9 +295,8 @@ module MilkTea
295
295
  end
296
296
 
297
297
  def current_word_prefix(uri, lsp_line, lsp_char)
298
- content = @workspace.get_content(uri)
299
- lines = content.split("\n", -1)
300
- line = lines[lsp_line] || ''
298
+ lines = @workspace.document_lines(uri)
299
+ line = lines[lsp_line] || ''
301
300
  # Walk backwards from cursor to find start of current word
302
301
  char_idx = [lsp_char - 1, line.length - 1].min
303
302
  return '' if char_idx < 0
@@ -284,6 +284,8 @@ module MilkTea
284
284
  def invalidate_cache(uri, clear_last_good: false)
285
285
  @tokens_cache.delete(uri)
286
286
  @ast_cache.delete(uri)
287
+ @definition_token_index.delete(uri)
288
+ @line_cache.delete(uri)
287
289
  @symbols_cache.delete(uri)
288
290
  @doc_comments_cache.delete(uri)
289
291
  @facts_cache_mutex.synchronize do
@@ -48,7 +48,6 @@ module MilkTea
48
48
  private
49
49
 
50
50
  def candidate_definition_uris(name, exclude_uri: nil)
51
- matcher = definition_line_matcher(name)
52
51
  open_uris, indexed_uris = @document_state_mutex.synchronize do
53
52
  [@open_documents.keys, @indexed_documents.keys]
54
53
  end
@@ -57,33 +56,33 @@ module MilkTea
57
56
  warmed_candidates = nil
58
57
  warmed_uris = nil
59
58
  @definition_cache_mutex.synchronize do
60
- warmed_candidates = @definition_candidate_uris[name].dup
59
+ warmed_candidates = @definition_candidate_uris[name].to_a
61
60
  warmed_uris = @definition_names_by_uri.keys.to_set
62
61
  end
63
62
 
64
- matches = warmed_candidates.to_a.filter_map do |doc_uri|
65
- next if doc_uri == exclude_uri
66
-
67
- doc_uri
68
- end
63
+ matches = warmed_candidates.reject { |doc_uri| doc_uri == exclude_uri }
69
64
 
65
+ # One-time lazy index: extract each document's definition names and
66
+ # cache them so repeat lookups are Set-membership checks instead of
67
+ # re-scanning every indexed document's full content with a regex on
68
+ # each lookup (which scaled with total workspace bytes).
70
69
  ordered_uris.each do |doc_uri|
71
70
  next if doc_uri == exclude_uri
72
71
  next if warmed_uris.include?(doc_uri)
73
72
 
74
73
  content = get_content(doc_uri)
75
74
  next if content.empty?
76
- next unless content.match?(matcher)
77
75
 
78
76
  warm_definition_candidates_for_uri(doc_uri, content)
79
- matches << doc_uri
80
77
  end
81
78
 
82
- matches.uniq
83
- end
79
+ @definition_cache_mutex.synchronize do
80
+ @definition_candidate_uris[name].each do |doc_uri|
81
+ matches << doc_uri unless doc_uri == exclude_uri
82
+ end
83
+ end
84
84
 
85
- def definition_line_matcher(name)
86
- /#{DEFINITION_LINE_PREFIX}#{Regexp.escape(name)}\b/
85
+ matches.uniq
87
86
  end
88
87
 
89
88
  def cache_definition_entry(name, entry)
@@ -126,13 +126,18 @@ module MilkTea
126
126
  total = paths.length
127
127
  paths.each_with_index do |path, idx|
128
128
  file_uri = path_to_uri(path)
129
+ content = nil
129
130
  @document_state_mutex.synchronize do
130
- @indexed_documents[file_uri] ||= begin
131
+ content = @indexed_documents[file_uri] ||= begin
131
132
  File.read(path)
132
133
  rescue StandardError
133
134
  nil
134
135
  end
135
136
  end
137
+ # Warm the definition-name index for this file so later global
138
+ # definition lookups hit Set-membership checks instead of scanning
139
+ # every indexed document's content.
140
+ warm_definition_candidates_for_uri(file_uri, content) if content
136
141
  if progress && total > 0
137
142
  pct = ((idx + 1) * 100 / total).clamp(0, 100)
138
143
  progress.call(pct, "#{idx + 1}/#{total} files")
@@ -79,10 +79,9 @@ module MilkTea
79
79
  # Scan text up to the cursor to find the innermost open function call context.
80
80
  # Returns { name:, active_parameter: } or nil if not inside a call.
81
81
  def find_call_context(uri, lsp_line, lsp_char)
82
- content = get_content(uri)
83
- return nil if content.empty?
82
+ lines = document_lines(uri)
83
+ return nil if lines.empty?
84
84
 
85
- lines = content.split("\n", -1)
86
85
  cursor_line = lines[lsp_line] || ''
87
86
  prefix = lsp_line.positive? ? lines[0...lsp_line].join("\n") + "\n" : ''
88
87
  text = prefix + cursor_line[0...lsp_char]
@@ -150,14 +149,19 @@ module MilkTea
150
149
  end
151
150
 
152
151
  def token_contains_position?(token, target_line, target_char)
153
- segments = token.lexeme.split("\n", -1)
154
- end_line = token.line + segments.length - 1
155
- return false if target_line < token.line || target_line > end_line
152
+ return false if target_line < token.line
153
+
154
+ lexeme = token.lexeme
155
+ unless lexeme.include?("\n")
156
+ return false if target_line > token.line
156
157
 
157
- if segments.length == 1
158
- return token.column <= target_char && target_char < (token.column + segments.first.length)
158
+ return token.column <= target_char && target_char < (token.column + lexeme.length)
159
159
  end
160
160
 
161
+ segments = lexeme.split("\n", -1)
162
+ end_line = token.line + segments.length - 1
163
+ return false if target_line > end_line
164
+
161
165
  if target_line == token.line
162
166
  return token.column <= target_char && target_char <= (token.column + segments.first.length - 1)
163
167
  end
@@ -178,8 +182,7 @@ module MilkTea
178
182
  end
179
183
 
180
184
  def find_dot_receiver_path(uri, lsp_line, lsp_char)
181
- content = get_content(uri)
182
- lines = content.split("\n", -1)
185
+ lines = document_lines(uri)
183
186
  line_str = lines[lsp_line] || ''
184
187
 
185
188
  idx = [lsp_char - 1, line_str.length - 1].min
@@ -222,39 +225,56 @@ module MilkTea
222
225
  # (def, struct, union, enum, flags, variant, type, const, var) for the given name.
223
226
  # Returns the identifier Token, or nil if not found.
224
227
  def find_definition_token(uri, name, before_line: nil, before_char: nil)
225
- tokens = get_tokens(uri)
226
- return nil if tokens.nil?
227
-
228
- nearest = nil
229
- tokens.each_cons(2) do |kw_tok, id_tok|
230
- next unless DEFINITION_KEYWORDS.include?(kw_tok.type)
231
- next unless id_tok.type == :identifier && id_tok.lexeme == name
228
+ candidates = definition_token_index(uri)[name]
229
+ return nil if candidates.nil? || candidates.empty?
232
230
 
233
- if before_line
234
- next if id_tok.line > before_line
235
- next if id_tok.line == before_line && before_char && id_tok.column >= before_char
236
- end
237
-
238
- if nearest.nil? || id_tok.line > nearest.line || (id_tok.line == nearest.line && id_tok.column > nearest.column)
239
- nearest = id_tok
231
+ if before_line
232
+ matches = candidates.select do |tok|
233
+ tok.line < before_line || (tok.line == before_line && (!before_char || tok.column < before_char))
240
234
  end
235
+ return matches.max_by { |tok| [tok.line, tok.column] } unless matches.empty?
241
236
  end
242
237
 
243
- return nearest if nearest
238
+ candidates.first
239
+ end
244
240
 
245
- tokens.each_cons(2) do |kw_tok, id_tok|
246
- next unless DEFINITION_KEYWORDS.include?(kw_tok.type)
247
- next unless id_tok.type == :identifier && id_tok.lexeme == name
241
+ # Return the cached line array for +uri+, so hot request paths (e.g.
242
+ # completion) do not re-split the whole document once per helper.
243
+ def document_lines(uri)
244
+ @document_state_mutex.synchronize do
245
+ return @line_cache[uri] if @line_cache.key?(uri)
246
+ end
248
247
 
249
- return id_tok
248
+ lines = get_content(uri).split("\n", -1)
249
+ @document_state_mutex.synchronize do
250
+ @line_cache[uri] = lines
250
251
  end
251
- nil
252
+ lines
252
253
  end
253
254
 
254
255
  private
255
256
 
256
257
  # ── Symbol extraction (token-based, no AST position requirement) ────────
257
258
 
259
+ # Lazily build a per-uri map of definition identifier tokens keyed by
260
+ # name, so repeated definition lookups are O(definitions-with-name)
261
+ # instead of re-scanning the document's full token stream each time.
262
+ def definition_token_index(uri)
263
+ @definition_token_index[uri] ||= begin
264
+ tokens = get_tokens(uri)
265
+ index = Hash.new { |hash, key| hash[key] = [] }
266
+ if tokens
267
+ tokens.each_cons(2) do |kw_tok, id_tok|
268
+ next unless DEFINITION_KEYWORDS.include?(kw_tok.type)
269
+ next unless id_tok.type == :identifier
270
+
271
+ index[id_tok.lexeme] << id_tok
272
+ end
273
+ end
274
+ index
275
+ end
276
+ end
277
+
258
278
  def extract_symbols_from_tokens(uri)
259
279
  tokens = get_tokens(uri)
260
280
  return [] if tokens.nil?
@@ -37,7 +37,7 @@ module MilkTea
37
37
  # adjacent tokens, and the inner keyword (e.g. :function) is always followed
38
38
  # by the identifier. If the lexer ever merges a compound keyword into a
39
39
  # single token type (e.g. :const_function), it must be added here.
40
- DEFINITION_KEYWORDS = %i[function struct union enum flags variant type const var let extending opaque interface event].freeze
40
+ DEFINITION_KEYWORDS = %i[function struct union enum flags variant type const var let extending opaque interface event].to_set.freeze
41
41
  DOC_COMMENT_PREFIX = '##'
42
42
  DOC_TAG_PATTERN = /\A\s*@([A-Za-z_][A-Za-z0-9_-]*)(?:\s+(.*))?\z/
43
43
  DEFINITION_LINE_PREFIX = /^(?:\s)*(?:(?:public|foreign|external)\s+)*(?:function|struct|union|enum|flags|variant|type|const|var|let|extending|opaque|interface|event)\s+/m
@@ -55,6 +55,8 @@ module MilkTea
55
55
  @tokens_cache = {} # uri -> [Token]
56
56
  @last_good_tokens_cache = {} # uri -> last known-good [Token]
57
57
  @ast_cache = {} # uri -> AST::SourceFile (nil on parse failure)
58
+ @definition_token_index = {} # uri -> { name => [Token] } definition tokens by name
59
+ @line_cache = {} # uri -> [String] split lines, refreshed on invalidation
58
60
  @facts_cache = {} # uri -> SemanticAnalyzer::Facts (projection of cached tooling snapshot facts)
59
61
  @tooling_snapshot_cache = {} # uri -> SemanticAnalyzer::ToolingSnapshot (facts may be nil on structural failure)
60
62
  @symbols_cache = {} # uri -> [{name, kind, line, column}]
@@ -103,6 +105,8 @@ module MilkTea
103
105
  @tokens_cache.clear
104
106
  @last_good_tokens_cache.clear
105
107
  @ast_cache.clear
108
+ @definition_token_index.clear
109
+ @line_cache.clear
106
110
  @facts_cache.clear
107
111
  @tooling_snapshot_cache.clear
108
112
  @symbols_cache.clear
@@ -72,11 +72,12 @@ module MilkTea
72
72
  argv: @argv.dup,
73
73
  **options.except(:timings)
74
74
  )
75
- unless @out.equal?($stdout) || preview_notice_emitted
75
+ live = $stdout.tty?
76
+ unless (@out.equal?($stdout) && live) || preview_notice_emitted
76
77
  @out.write(result.stdout)
77
78
  end
78
79
  @out.flush if @out.respond_to?(:flush)
79
- @err.write(result.stderr) unless @err.equal?($stderr)
80
+ @err.write(result.stderr) unless @err.equal?($stderr) && live
80
81
  info("[cached]") if result.cached
81
82
  result.exit_status
82
83
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mt-lang
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Long (Teefan) Tran
@@ -629,7 +629,7 @@ metadata:
629
629
  homepage_uri: https://teefan.github.io/mt-lang/
630
630
  source_code_uri: https://github.com/teefan/mt-lang
631
631
  post_install_message: |
632
- Milk Tea 0.4.0 installed!
632
+ Milk Tea 0.4.2 installed!
633
633
 
634
634
  System requirements:
635
635
  - A C compiler (gcc or clang) must be available on PATH