mt-lang 0.2.7 → 0.2.10

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: b02adcca2cd2665b97f3a32f18130ed355dad80950220475ff623ccc6254250b
4
- data.tar.gz: f23de0c1d00fc7f9bd8f6275a921c2b92824b3920425b3fc9f0e1734ecfe2a1a
3
+ metadata.gz: 9047d861f94f1e5b66f531712466e32779a0a86bd02db452e36cd8990980ee5c
4
+ data.tar.gz: 3e9d1c7f06e70e2a9e3b84b8df4e8ee64b92be6e34f6bc970ca0b2ef69683c8e
5
5
  SHA512:
6
- metadata.gz: f29063940f36a621591a64f017e3357aef18db3b7f576f1a0af056514ffee08b67bf1ffd45dc6c2066fd6f27103b5a8e3bced0139415dd558a3e3a0dad5a7cc9
7
- data.tar.gz: 4a07fd8a9469cec594b55a41fd7bb7f6220a5baa8653ec4a297d5e06ac80e79f4e1a3b4b69cde79185acb2863d1fe7372e5c9cd2d439a95d3bbe46423d5e2c9d
6
+ metadata.gz: 4144e09654664b0dd4eaf7d697fe077381c965902564297d32002807be4b9100861ca8c7c7e065b983e90b91dba4b86f22f52eaa4eae47fa1d68dee1cb1acc36
7
+ data.tar.gz: a712ee592463d961523bd2c8481be6993843f277c51bd3947f34bd8d3e32ae8d245ead8985f31d2621cefd8cc1ec705eba1749aca99324cdd3061adb5ef13ce2
data/lib/milk_tea/base.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module MilkTea
6
- VERSION = "0.2.7"
6
+ VERSION = "0.2.10"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -227,8 +227,8 @@ module MilkTea
227
227
  RangeExpr = Data.define(:start_expr, :end_expr, :line, :column)
228
228
  ExpressionList = Data.define(:elements, :line, :column)
229
229
  IfExpr = Data.define(:condition, :then_expression, :else_expression)
230
- MatchExpr = Data.define(:expression, :arms, :line, :column, :length) do
231
- def initialize(expression:, arms:, line: nil, column: nil, length: nil) = super
230
+ MatchExpr = Data.define(:expression, :arms, :line, :column, :length, :desugared_from_is) do
231
+ def initialize(expression:, arms:, line: nil, column: nil, length: nil, desugared_from_is: false) = super
232
232
  end
233
233
  UnsafeExpr = Data.define(:expression, :line, :column, :length) do
234
234
  def initialize(expression:, line: nil, column: nil, length: nil) = super
@@ -80,7 +80,7 @@ module MilkTea
80
80
  def resolve_extending_receiver_type(analysis, type_name)
81
81
  if type_name.is_a?(AST::TypeRef)
82
82
  generic_type = resolve_named_generic_type_for_analysis(analysis, type_name.name.parts)
83
- if generic_type.is_a?(Types::GenericStructDefinition)
83
+ if generic_type.is_a?(Types::GenericStructDefinition) || generic_type.is_a?(Types::GenericVariantDefinition)
84
84
  validate_methods_receiver_type_arguments!(type_name, generic_type)
85
85
  return generic_type
86
86
  end
@@ -430,6 +430,8 @@ module MilkTea
430
430
  [:get, nil, nil, nil]
431
431
  elsif (type = @ctx.types[callee.name]).is_a?(Types::Struct) || type.is_a?(Types::StringView) || task_type?(type) || type.is_a?(Types::Vector) || type.is_a?(Types::Matrix) || type.is_a?(Types::Quaternion)
432
432
  [ :struct_literal, nil, nil, type ]
433
+ elsif (type = @ctx.types[callee.name]).is_a?(Types::GenericStructDefinition) || type.is_a?(Types::GenericVariantDefinition)
434
+ raise LoweringError, "generic type #{callee.name} requires type arguments"
433
435
  else
434
436
  emit_fn = @artifacts.emitted_declarations.find { |d| d.is_a?(IR::Function) && d.name == callee.name }
435
437
  if emit_fn
@@ -452,6 +454,10 @@ module MilkTea
452
454
  return [:function, external_function_c_name(binding), nil, binding.type, binding]
453
455
  end
454
456
  imported_type = imported_module.types[callee.member]
457
+ if imported_type.is_a?(Types::GenericStructDefinition) || imported_type.is_a?(Types::GenericVariantDefinition)
458
+ raise LoweringError, "generic type #{callee.receiver.name}.#{callee.member} requires type arguments"
459
+ end
460
+
455
461
  if imported_type.is_a?(Types::Struct) || imported_type.is_a?(Types::StringView) || task_type?(imported_type) || imported_type.is_a?(Types::Vector) || imported_type.is_a?(Types::Matrix) || imported_type.is_a?(Types::Quaternion)
456
462
  return [:struct_literal, nil, nil, imported_module.types.fetch(callee.member)]
457
463
  end
@@ -141,6 +141,7 @@ module MilkTea
141
141
  line:,
142
142
  column:,
143
143
  length: previous.lexeme.length,
144
+ desugared_from_is: true,
144
145
  )
145
146
  end
146
147
  expr
@@ -1005,7 +1005,7 @@ module MilkTea
1005
1005
  )
1006
1006
 
1007
1007
  check_function_call(callable, expression.arguments, scopes:)
1008
- callable.owner.send(:check_function, callable) unless callable.type_arguments.empty?
1008
+ validate_specialized_function_body(callable) unless callable.type_arguments.empty?
1009
1009
  callable.type.return_type
1010
1010
  when :method
1011
1011
  callable = specialize_function_binding(
@@ -1018,7 +1018,7 @@ module MilkTea
1018
1018
  raise_sema_error("cannot call editable method #{callable.name} on an immutable receiver") if callable.type.receiver_editable && !assignable_receiver?(receiver, scopes)
1019
1019
 
1020
1020
  check_function_call(callable, expression.arguments, scopes:)
1021
- callable.owner.send(:check_function, callable) unless callable.type_arguments.empty?
1021
+ validate_specialized_function_body(callable) unless callable.type_arguments.empty?
1022
1022
  callable.type.return_type
1023
1023
  when :callable_value
1024
1024
  check_callable_value_call(callable, expression.arguments, scopes:, callee_expression: expression.callee)
@@ -256,6 +256,22 @@ module MilkTea
256
256
  end
257
257
  end
258
258
 
259
+ # Validates the body of a specialized (instantiated) function or method
260
+ # binding. The owner checker may be in collecting-errors mode, which
261
+ # would silently swallow body errors into @structural_errors. We
262
+ # temporarily disable collect mode on the owner so the caller receives
263
+ # the SemanticError directly.
264
+ def validate_specialized_function_body(binding)
265
+ owner = binding.owner
266
+ prev_collecting = owner.instance_variable_get(:@collecting_errors)
267
+ owner.instance_variable_set(:@collecting_errors, false)
268
+ owner.send(:check_function, binding)
269
+ rescue SemanticError => e
270
+ raise unless e.message.include?("cannot assign through immutable")
271
+ ensure
272
+ owner.instance_variable_set(:@collecting_errors, prev_collecting) if owner
273
+ end
274
+
259
275
  # Per-function error collection used by check_collecting_errors.
260
276
  # Continues past individual function failures, accumulating SemanticErrors.
261
277
  def check_functions_collecting(errors)
@@ -286,13 +302,29 @@ module MilkTea
286
302
  end
287
303
  end
288
304
 
305
+ # Scans generic method bodies for assignments to this through a
306
+ # non-editable receiver. Full body checking is deferred to call-site
307
+ # specialization, but immutable-this violations are type-independent.
308
+ def check_generic_method_immutable_this(binding, scopes)
309
+ this_binding = scopes.first&.values&.find { |v| v.name == "this" }
310
+ return unless this_binding
311
+ return if this_binding.mutable
312
+
313
+ binding.ast.body&.each do |statement|
314
+ next unless statement.is_a?(AST::Assignment) && statement.operator == "="
315
+ next unless statement.target.is_a?(AST::MemberAccess) && statement.target.receiver.is_a?(AST::Identifier) && statement.target.receiver.name == "this"
316
+
317
+ raise_sema_error("cannot assign through immutable this", statement.target)
318
+ end
319
+ end
320
+
289
321
  def check_function(binding)
290
322
  @local_completion_frames = @local_completion_frames.dup if @local_completion_frames.frozen?
291
323
 
292
324
  previous_type_substitutions = @current_type_substitutions
293
325
  previous_specialization_owner = @current_specialization_owner
294
326
  started_check = false
295
- return if binding.external || binding.type_params.any?
327
+ return if binding.external
296
328
  return if @checked_function_bindings[binding.object_id]
297
329
  return if @checking_function_bindings[binding.object_id]
298
330
 
@@ -303,6 +335,11 @@ module MilkTea
303
335
  with_error_node(binding.ast) do
304
336
  with_scope(binding.body_params) do |scopes|
305
337
  start_local_completion_frame(binding, scopes)
338
+ if binding.type_params.any?
339
+ check_generic_method_immutable_this(binding, scopes)
340
+ return
341
+ end
342
+
306
343
  if binding.ast.is_a?(AST::ForeignFunctionDecl)
307
344
  record_callable_value_expression_site(binding.ast.mapping) unless binding.ast.mapping.is_a?(AST::Call)
308
345
  expression = foreign_mapping_expression(binding.ast)
@@ -337,10 +374,10 @@ module MilkTea
337
374
  end
338
375
  end
339
376
  end
340
- @checked_function_bindings[binding.object_id] = true
341
377
  ensure
342
378
  return unless started_check
343
379
 
380
+ @checked_function_bindings[binding.object_id] = true
344
381
  finish_local_completion_frame(binding)
345
382
  @preassigned_local_binding_ids = {}
346
383
  @nullability_flow_result = nil
@@ -304,10 +304,17 @@ module MilkTea
304
304
  end
305
305
  display_name = type_name.is_a?(Array) ? type_name.join(".") : type_name
306
306
  raise_sema_error("unknown type #{display_name} for struct destructure") unless struct_type
307
- raise_sema_error("#{display_name} is not a struct") unless struct_type.is_a?(Types::Struct) || struct_type.is_a?(Types::Tuple)
308
- ensure_assignable!(value_type, struct_type, "cannot destructure #{value_type} as #{display_name}")
309
-
310
- fields = struct_type.fields
307
+ if struct_type.is_a?(Types::GenericStructDefinition)
308
+ if value_type.is_a?(Types::StructInstance) && value_type.definition == struct_type
309
+ fields = value_type.fields
310
+ else
311
+ raise_sema_error("generic type #{display_name} requires type arguments")
312
+ end
313
+ else
314
+ raise_sema_error("#{display_name} is not a struct") unless struct_type.is_a?(Types::Struct) || struct_type.is_a?(Types::Tuple)
315
+ ensure_assignable!(value_type, struct_type, "cannot destructure #{value_type} as #{display_name}")
316
+ fields = struct_type.fields
317
+ end
311
318
  raise_sema_error("destructure pattern has #{statement.destructure_bindings.length} bindings but #{display_name} has #{fields.length} fields") unless statement.destructure_bindings.length == fields.length
312
319
 
313
320
  current_scope = current_actual_scope(scopes)
@@ -36,7 +36,7 @@ module MilkTea
36
36
 
37
37
  def to_diagnostic(path: nil)
38
38
  Diagnostic.new(
39
- path:,
39
+ path: @path || path,
40
40
  line: @line,
41
41
  column: @column,
42
42
  length: @length,
@@ -87,8 +87,17 @@ module MilkTea
87
87
  sema_start = total_start ? monotonic_time : nil
88
88
  sema_snapshot ||= SemanticAnalyzer.tooling_snapshot(ast, imported_modules: imported_modules.fetch(:modules), path: path, allow_missing_imports: true)
89
89
  sema_facts = sema_snapshot.facts
90
+ cross_by_uri = {}
91
+ target_path = path.is_a?(String) ? File.expand_path(path) : nil
90
92
  sema_snapshot.diagnostics.reject { |diagnostic| redundant_unknown_import_diagnostic?(diagnostic, unresolved_import_paths) }
91
- .each { |diagnostic| diagnostics << format_tooling_diagnostic(diagnostic, stage: 'sema') }
93
+ .each do |diagnostic|
94
+ if target_path && diagnostic.path && File.expand_path(diagnostic.path) != target_path
95
+ target_uri = path_to_uri(diagnostic.path)
96
+ (cross_by_uri[target_uri] ||= []) << format_tooling_diagnostic(diagnostic, stage: 'sema')
97
+ else
98
+ diagnostics << format_tooling_diagnostic(diagnostic, stage: 'sema')
99
+ end
100
+ end
92
101
  sema_ms = elapsed_ms(sema_start) if sema_start
93
102
  rescue StandardError => e
94
103
  log_diagnostics_warning("Error collecting diagnostics: #{e.message}")
@@ -142,7 +151,7 @@ module MilkTea
142
151
  log_diagnostics_warning("Error collecting lint diagnostics: #{e.message}")
143
152
  end
144
153
 
145
- { diagnostics: diagnostics, facts: sema_facts, sema_snapshot: sema_snapshot }
154
+ { diagnostics: diagnostics, cross_file_diagnostics: cross_by_uri, facts: sema_facts, sema_snapshot: sema_snapshot }
146
155
  ensure
147
156
  if total_start
148
157
  lint_breakdown = lint_profile&.summary(limit: 12)
@@ -196,7 +205,15 @@ module MilkTea
196
205
  def self.redundant_unknown_import_diagnostic?(diagnostic, unresolved_import_paths)
197
206
  return false unless diagnostic&.message.to_s.start_with?("unknown import ")
198
207
 
199
- unresolved_import_paths.include?(diagnostic.message.delete_prefix("unknown import "))
208
+ import_path = diagnostic.message.to_s.match(/unknown import (.+)$/)&.captures&.first
209
+ return false unless import_path
210
+
211
+ unresolved_import_paths.include?(import_path)
212
+ end
213
+
214
+ def self.path_to_uri(path)
215
+ escaped_path = path.split('/').map { |seg| CGI.escape(seg).gsub('+', '%20') }.join('/')
216
+ "file://#{escaped_path}"
200
217
  end
201
218
 
202
219
  def self.uri_to_path(uri)
@@ -366,7 +366,8 @@ module MilkTea
366
366
  return { kind: 'full', items: [] } unless uri
367
367
 
368
368
  content = @workspace.get_content(uri)
369
- diagnostics = @workspace.collect_diagnostics(uri)
369
+ result = @workspace.collect_diagnostics(uri)
370
+ diagnostics = result
370
371
  fingerprint = diagnostics_fingerprint(content, diagnostics)
371
372
  previous_result_id = params['previousResultId']
372
373
  cached = @diagnostic_report_cache[uri]
@@ -384,6 +385,16 @@ module MilkTea
384
385
  fingerprint: fingerprint
385
386
  }
386
387
 
388
+ cross = @workspace.cross_file_diagnostics
389
+ if cross&.any?
390
+ cross.each do |target_uri, items|
391
+ @protocol.write_notification('textDocument/publishDiagnostics', {
392
+ uri: target_uri,
393
+ diagnostics: items
394
+ })
395
+ end
396
+ end
397
+
387
398
  {
388
399
  kind: 'full',
389
400
  resultId: result_id,
@@ -473,7 +473,7 @@ module MilkTea
473
473
 
474
474
  current_type = first_type
475
475
  chain_segments[1..].each do |seg|
476
- field_owner = project_field_receiver_type_for_completion(current_type)
476
+ field_owner = project_field_receiver_type_for_completion(current_type, facts)
477
477
  if field_owner.respond_to?(:field) && (field_type = field_owner.field(seg))
478
478
  current_type = field_type
479
479
  else
@@ -674,7 +674,7 @@ module MilkTea
674
674
  def completion_items_for_value_receiver(facts, receiver_type, prefix)
675
675
  items = []
676
676
 
677
- field_receiver_type = project_field_receiver_type_for_completion(receiver_type)
677
+ field_receiver_type = project_field_receiver_type_for_completion(receiver_type, facts)
678
678
  if field_receiver_type.respond_to?(:fields)
679
679
  field_receiver_type.fields.each do |fname, ftype|
680
680
  next unless prefix.empty? || fname.start_with?(prefix)
@@ -763,8 +763,20 @@ module MilkTea
763
763
  dispatch_receiver_type == receiver_type ? receiver_type : dispatch_receiver_type
764
764
  end
765
765
 
766
- def project_field_receiver_type_for_completion(type)
767
- project_receiver_type_for_completion(type)
766
+ def project_field_receiver_type_for_completion(type, facts = nil)
767
+ type = project_receiver_type_for_completion(type)
768
+ return type.definition if type.is_a?(Types::StructInstance)
769
+ return field_type_from_struct_definition(type, facts) if facts
770
+
771
+ type
772
+ end
773
+
774
+ def field_type_from_struct_definition(type, facts)
775
+ return type unless type.is_a?(Types::GenericInstance)
776
+ return type if ref_type_name?(type) || pointer_type_name?(type)
777
+
778
+ struct_def = facts.types[type.name]
779
+ struct_def if struct_def&.respond_to?(:fields)
768
780
  end
769
781
 
770
782
  def project_method_receiver_type_for_completion(type)
@@ -778,8 +790,8 @@ module MilkTea
778
790
  type
779
791
  end
780
792
 
781
- def field_owner_type(receiver_type)
782
- aggregate_type = project_field_receiver_type_for_completion(receiver_type)
793
+ def field_owner_type(receiver_type, facts = nil)
794
+ aggregate_type = project_field_receiver_type_for_completion(receiver_type, facts)
783
795
  return aggregate_type.definition if aggregate_type.is_a?(Types::StructInstance)
784
796
 
785
797
  aggregate_type
@@ -247,7 +247,7 @@ module MilkTea
247
247
  return nil unless current_type
248
248
 
249
249
  chain[:segments][1..hovered_segment[:position]].each do |segment|
250
- field_receiver_type = project_field_receiver_type_for_completion(current_type)
250
+ field_receiver_type = project_field_receiver_type_for_completion(current_type, facts)
251
251
  if field_receiver_type.respond_to?(:field) && (field_type = field_receiver_type.field(segment[:name]))
252
252
  return field_definition_location(current_uri, field_receiver_type, segment[:name]) if segment[:token_index] == token_index
253
253
 
@@ -133,6 +133,16 @@ module MilkTea
133
133
  diagnostics: diagnostics
134
134
  })
135
135
  notify_diagnostic_errors(uri, diagnostics)
136
+
137
+ cross = @workspace.cross_file_diagnostics
138
+ if cross&.any?
139
+ cross.each do |target_uri, items|
140
+ @protocol.write_notification('textDocument/publishDiagnostics', {
141
+ uri: target_uri,
142
+ diagnostics: items
143
+ })
144
+ end
145
+ end
136
146
  end
137
147
  elsif perf_logging?
138
148
  @diagnostics_perf[:dropped_stale] += 1
@@ -8,19 +8,222 @@ module MilkTea
8
8
 
9
9
  KEYWORD_HOVER_INFO = {
10
10
  'size_of' => {
11
- signature: 'size_of(expr) -> int',
12
- docs: '`size_of(expr)` evaluates the compile-time size (in bytes) of the expression\'s type.',
11
+ signature: 'size_of(Type) -> ptr_uint',
12
+ docs: '`size_of(Type)` evaluates the compile-time size (in bytes) of the type.',
13
13
  },
14
14
  'align_of' => {
15
- signature: 'align_of(expr) -> int',
16
- docs: '`align_of(expr)` evaluates the compile-time alignment (in bytes) of the expression\'s type.',
15
+ signature: 'align_of(Type) -> ptr_uint',
16
+ docs: '`align_of(Type)` evaluates the compile-time alignment (in bytes) of the type.',
17
17
  },
18
18
  'offset_of' => {
19
- signature: 'offset_of(Type, field) -> int',
19
+ signature: 'offset_of(Type, field) -> ptr_uint',
20
20
  docs: '`offset_of(Type, field)` evaluates the compile-time byte offset of `field` within `Type`.',
21
21
  },
22
22
  }.freeze
23
23
 
24
+ LANGUAGE_KEYWORD_HOVER_INFO = {
25
+ 'let' => {
26
+ signature: 'let',
27
+ docs: 'Immutable local variable declaration: `let name = value` or `let name: Type`. Supports `else` guard with `T?`, `Option[T]`, or `Result[T, E]`.',
28
+ },
29
+ 'var' => {
30
+ signature: 'var',
31
+ docs: 'Mutable local or module-level variable: `var name: Type = initializer`. Supports `else` guard. Module `var` requires explicit type.',
32
+ },
33
+ 'const' => {
34
+ signature: 'const',
35
+ docs: 'Compile-time constant, requires explicit type and initializer. Block-bodied form `const NAME -> TYPE:` evaluates at compile time.',
36
+ },
37
+ 'function' => {
38
+ signature: 'function',
39
+ docs: 'Ordinary function declaration: `function name(params) -> ReturnType:`. Parameters are non-rebindable. Return type defaults to `void`.',
40
+ },
41
+ 'async' => {
42
+ signature: 'async',
43
+ docs: 'Marks a function as async; the declared return type is lifted to `Task[T]`. Use `await` inside async functions.',
44
+ },
45
+ 'await' => {
46
+ signature: 'await',
47
+ docs: 'Awaits a `Task[T]` inside an async function, yielding the unwrapped `T`. Only allowed in async function bodies.',
48
+ },
49
+ 'struct' => {
50
+ signature: 'struct',
51
+ docs: 'Value-type struct with named, typed fields: `struct Name: field: Type`. Supports generics, nested structs, and `implements`.',
52
+ },
53
+ 'enum' => {
54
+ signature: 'enum',
55
+ docs: 'Integer-backed enumeration: `enum Name: BackingType`. Backing type defaults to `int`. Values auto-increment from 0 or the last explicit value.',
56
+ },
57
+ 'flags' => {
58
+ signature: 'flags',
59
+ docs: 'Bitmask type backed by an integer primitive: `flags Name: uint`. Members must be compile-time integer constants.',
60
+ },
61
+ 'union' => {
62
+ signature: 'union',
63
+ docs: 'Overlapped storage union: `union Name: field1: Type1, field2: Type2`. All fields share the same memory.',
64
+ },
65
+ 'variant' => {
66
+ signature: 'variant',
67
+ docs: 'Tagged union: `variant Name: arm1(field: Type), arm2`. Arms may carry named payload fields. No-payload arms are bare identifiers.',
68
+ },
69
+ 'opaque' => {
70
+ signature: 'opaque',
71
+ docs: 'Externally-defined type with unknown layout: `opaque Name`. May declare `implements` for interface conformance.',
72
+ },
73
+ 'type' => {
74
+ signature: 'type',
75
+ docs: 'Type alias: `type Name = ExistingType`. Generic type parameters are supported.',
76
+ },
77
+ 'interface' => {
78
+ signature: 'interface',
79
+ docs: 'Declares a method contract: `interface Name: function method(params) -> T`. Implemented via `implements`; used at runtime via `dyn[Interface]`.',
80
+ },
81
+ 'extending' => {
82
+ signature: 'extending',
83
+ docs: 'Extends an existing type with methods: `extending Type: function method():`. Supports `function`, `editable function`, and `static function`.',
84
+ },
85
+ 'implements' => {
86
+ signature: 'implements',
87
+ docs: 'Nominal interface conformance on `struct` or `opaque`: `struct Foo implements Interface`. Must be declared on the type definition.',
88
+ },
89
+ 'editable' => {
90
+ signature: 'editable',
91
+ docs: 'Method receiver modifier: `editable function`. Grants mutable access to `this`. Used in interfaces and extending blocks.',
92
+ },
93
+ 'static' => {
94
+ signature: 'static',
95
+ docs: 'Static method modifier: `static function`. No `this` receiver. Used in interfaces and extending blocks for constructors and utilities.',
96
+ },
97
+ 'import' => {
98
+ signature: 'import',
99
+ docs: 'Module import: `import module.path` or `import module.path as alias`. Module lookup resolves `a.b.c` to `a/b/c.mt`.',
100
+ },
101
+ 'public' => {
102
+ signature: 'public',
103
+ docs: 'Export visibility modifier: `public function`, `public type`, etc. Rejected on `extending`, `external`, and `static_assert` declarations.',
104
+ },
105
+ 'if' => {
106
+ signature: 'if',
107
+ docs: 'Conditional branch: `if condition:`. Condition must be `bool`. Supports `else if` and `else`. Inline form: `if cond: stmt else: stmt`.',
108
+ },
109
+ 'else' => {
110
+ signature: 'else',
111
+ docs: 'Else branch for `if`. Chains as `else if condition:` for additional branches. Supports inline single-statement form `else: stmt`.',
112
+ },
113
+ 'match' => {
114
+ signature: 'match',
115
+ docs: 'Pattern match on enum, variant, integer, `str`, or tuple. Expression form produces a value. Must be exhaustive without `_` wildcard.',
116
+ },
117
+ 'for' => {
118
+ signature: 'for',
119
+ docs: 'Loop: `for i in 0..count:` (exclusive range) or `for item in iterable:`. Multi-iteration `for left, right in xs, ys:` iterates arrays/spans in lockstep.',
120
+ },
121
+ 'while' => {
122
+ signature: 'while',
123
+ docs: 'Conditional loop: `while condition:`. Condition must be `bool`. Supports `break`, `continue`, and `defer` inside the body.',
124
+ },
125
+ 'return' => {
126
+ signature: 'return',
127
+ docs: 'Returns a value from a function. Not allowed inside `defer` blocks.',
128
+ },
129
+ 'break' => {
130
+ signature: 'break',
131
+ docs: 'Exits the nearest enclosing `for`, `while`, or `parallel for` loop.',
132
+ },
133
+ 'continue' => {
134
+ signature: 'continue',
135
+ docs: 'Skips to the next iteration of the nearest enclosing `for` or `while` loop.',
136
+ },
137
+ 'defer' => {
138
+ signature: 'defer',
139
+ docs: 'Defers an expression or block to function exit: `defer expr` or `defer:`. Multiple defers execute in LIFO order. `return` is not allowed inside.',
140
+ },
141
+ 'unsafe' => {
142
+ signature: 'unsafe',
143
+ docs: 'Required for pointer indexing, raw pointer dereference, pointer arithmetic, pointer casts, and `reinterpret[...]`. Single-expr or block form.',
144
+ },
145
+ 'external' => {
146
+ signature: 'external',
147
+ docs: 'Raw C ABI surface: `external function name(params) -> T`. No body, supports variadic `...`. Also marks external files with `external` header.',
148
+ },
149
+ 'foreign' => {
150
+ signature: 'foreign',
151
+ docs: 'Foreign function bridging: `foreign function name(params) -> T = c.FuncName`. Supports `in`, `out`, `inout`, and `consuming` parameter modes.',
152
+ },
153
+ 'consuming' => {
154
+ signature: 'consuming',
155
+ docs: 'Foreign function parameter mode: takes ownership of the argument. The caller\'s binding is consumed. Only on `foreign function` params.',
156
+ },
157
+ 'when' => {
158
+ signature: 'when',
159
+ docs: 'Compile-time conditional: `when CONSTANT:`. Only the chosen branch is type-checked and emitted. Requires `else` unless exhaustive.',
160
+ },
161
+ 'inline' => {
162
+ signature: 'inline',
163
+ docs: 'Compile-time unrolling modifier: `inline for` (loop unrolling), `inline while`, `inline match`, `inline if`. Only the active branch emits code.',
164
+ },
165
+ 'emit' => {
166
+ signature: 'emit',
167
+ docs: 'Emits a declaration at compile time: `emit function ...`. Only allowed inside `const function` or `inline` bodies.',
168
+ },
169
+ 'parallel' => {
170
+ signature: 'parallel',
171
+ docs: 'Concurrency construct: `parallel for i in 0..N:` (data-parallel loop) or `parallel:` block (concurrent statement dispatch). Uses OS threads via libuv.',
172
+ },
173
+ 'detach' => {
174
+ signature: 'detach',
175
+ docs: 'Spawns work on a separate thread, returning a `Handle`: `let h = detach func()`. Use `gather` to wait. Supports global function calls only.',
176
+ },
177
+ 'gather' => {
178
+ signature: 'gather',
179
+ docs: 'Blocks until one or more `detach` handles complete: `gather h1, h2`. Takes one or more `Handle` values.',
180
+ },
181
+ 'event' => {
182
+ signature: 'event',
183
+ docs: 'Typed publisher/subscriber: `event name[capacity]` or `event name[capacity](PayloadType)`. Supports `subscribe`, `emit`, `unsubscribe`, `wait`.',
184
+ },
185
+ 'attribute' => {
186
+ signature: 'attribute',
187
+ docs: 'Declares a reusable declaration attribute: `attribute[target, ...] name(params)`. Targets: struct, field, callable, const, event, enum, flags, union, variant.',
188
+ },
189
+ 'proc' => {
190
+ signature: 'proc',
191
+ docs: 'Ref-counted closure: `proc(params...) -> T: body`. Captures values by value. Storable in structs, arrays, and tuples.',
192
+ },
193
+ 'fn' => {
194
+ signature: 'fn',
195
+ docs: 'Function pointer type: `fn(params...) -> T`. Points to module-level functions. No captured state; capture-free.',
196
+ },
197
+ 'dyn' => {
198
+ signature: 'dyn',
199
+ docs: 'Runtime interface value: `dyn[Interface]`. A fat pointer carrying a data pointer and vtable. Constructed via `adapt[Interface](value)`.',
200
+ },
201
+ 'is' => {
202
+ signature: 'is',
203
+ docs: 'Variant arm membership test: `expr is Variant.arm`. Desugars to a `match` expression evaluating to `bool`. Supports `not` negation.',
204
+ },
205
+ 'pass' => {
206
+ signature: 'pass',
207
+ docs: 'Explicit no-op statement for intentionally empty block bodies.',
208
+ },
209
+ 'null' => {
210
+ signature: 'null',
211
+ docs: 'Null value for nullable types (`T?`). Use typed `null[T]` when context cannot determine the target type.',
212
+ },
213
+ 'true' => {
214
+ signature: 'true',
215
+ docs: 'Boolean literal `true`. Type is `bool`.',
216
+ },
217
+ 'false' => {
218
+ signature: 'false',
219
+ docs: 'Boolean literal `false`. Type is `bool`.',
220
+ },
221
+ 'static_assert' => {
222
+ signature: 'static_assert',
223
+ docs: 'Compile-time assertion: `static_assert(condition, message)`. Fails compilation if the compile-time condition is false.',
224
+ },
225
+ }.freeze
226
+
24
227
  def handle_hover(params)
25
228
  stages = new_perf_stages
26
229
  total_start = stages ? monotonic_time : nil
@@ -649,7 +852,7 @@ module MilkTea
649
852
  return nil unless current_type
650
853
 
651
854
  chain[:segments][1..hovered_segment[:position]].each do |segment|
652
- field_receiver_type = project_field_receiver_type_for_completion(current_type)
855
+ field_receiver_type = project_field_receiver_type_for_completion(current_type, facts)
653
856
  if field_receiver_type.respond_to?(:field) && (field_type = field_receiver_type.field(segment[:name]))
654
857
  source_location = field_definition_location(current_uri, field_receiver_type, segment[:name])
655
858
 
@@ -787,7 +990,7 @@ module MilkTea
787
990
  return nil unless receiver_info
788
991
 
789
992
  field_name = tokens[token_index].lexeme
790
- receiver_type = project_field_receiver_type_for_completion(receiver_info[:type])
993
+ receiver_type = project_field_receiver_type_for_completion(receiver_info[:type], facts)
791
994
  return nil unless receiver_type.respond_to?(:field)
792
995
 
793
996
  field_type = receiver_type.field(field_name)
@@ -809,7 +1012,7 @@ module MilkTea
809
1012
 
810
1013
  field_name = tokens[token_index].lexeme
811
1014
  if receiver_info
812
- receiver_type = project_field_receiver_type_for_completion(receiver_info[:type])
1015
+ receiver_type = project_field_receiver_type_for_completion(receiver_info[:type], facts)
813
1016
  if receiver_type.respond_to?(:field)
814
1017
  field_type = receiver_type.field(field_name)
815
1018
  if field_type
@@ -1260,10 +1463,9 @@ module MilkTea
1260
1463
  return nil unless token
1261
1464
 
1262
1465
  info = KEYWORD_HOVER_INFO[token.lexeme]
1263
- return nil unless info
1264
- return nil unless [:size_of, :align_of, :offset_of].include?(token.type)
1466
+ return info if info && [:size_of, :align_of, :offset_of].include?(token.type)
1265
1467
 
1266
- info
1468
+ LANGUAGE_KEYWORD_HOVER_INFO[token.lexeme]
1267
1469
  end
1268
1470
 
1269
1471
  def render_builtin_specialization(tokens)
@@ -1421,7 +1623,7 @@ module MilkTea
1421
1623
  def_index = tokens.index(definition_token)
1422
1624
  if def_index
1423
1625
  prev = previous_non_trivia_token_index(tokens, def_index)
1424
- if prev && tokens[prev].type == :identifier
1626
+ if prev
1425
1627
  case tokens[prev].lexeme
1426
1628
  when "let" then kind = :let
1427
1629
  when "var" then kind = :var
@@ -961,7 +961,7 @@ module MilkTea
961
961
  resolve_receiver_value_type(facts, receiver_tok).then do |receiver_type|
962
962
  next false unless receiver_type
963
963
 
964
- field_receiver_type = project_field_receiver_type_for_completion(receiver_type)
964
+ field_receiver_type = project_field_receiver_type_for_completion(receiver_type, facts)
965
965
  next false unless field_receiver_type.respond_to?(:field)
966
966
 
967
967
  callable_semantic_type?(field_receiver_type.field(name))
@@ -58,7 +58,7 @@ module MilkTea
58
58
  KEYWORD_TOKEN_TYPES = Token::KEYWORDS.values.to_set.freeze
59
59
  DEFAULT_LIBRARY_TYPE_NAMES = Types::BUILTIN_TYPE_NAMES.to_set.freeze
60
60
  BUILTIN_FUNCTION_NAMES = %w[
61
- ref_of const_ptr_of ptr_of read fatal reinterpret array span zero default adapt get
61
+ ref_of const_ptr_of ptr_of read fatal reinterpret array span zero default adapt get field_of callable_of has_attribute attribute_of attribute_arg fields_of members_of attributes_of
62
62
  ].to_set.freeze
63
63
  BUILTIN_ASSOCIATED_HOOK_NAMES = %w[hash equal order].to_set.freeze
64
64
  BUILTIN_CALL_HOVER_INFO = {
@@ -68,7 +68,7 @@ module MilkTea
68
68
  },
69
69
  'ref_of' => {
70
70
  signature: 'builtin ref_of(value) -> ref[T]',
71
- docs: '`ref_of(x)` borrows a mutable safe lvalue as `ref[T]`.'
71
+ docs: '`ref_of(x)` borrows a safe lvalue as `ref[T]`.'
72
72
  },
73
73
  'const_ptr_of' => {
74
74
  signature: 'builtin const_ptr_of(value) -> const_ptr[T]',
@@ -76,7 +76,7 @@ module MilkTea
76
76
  },
77
77
  'ptr_of' => {
78
78
  signature: 'builtin ptr_of(value) -> ptr[T]',
79
- docs: '`ptr_of(x)` takes the address of a mutable safe lvalue as `ptr[T]`.'
79
+ docs: '`ptr_of(x)` takes the address of a safe lvalue as `ptr[T]`.'
80
80
  },
81
81
  'read' => {
82
82
  signature: 'builtin read(value) -> T',
@@ -130,6 +130,18 @@ module MilkTea
130
130
  signature: 'builtin attribute_of(target, attribute_name) -> attribute_handle',
131
131
  docs: '`attribute_of(target, attribute_name)` returns the applied attribute handle for the resolved target-and-attribute pair; use `has_attribute(...)` when absence is expected.'
132
132
  },
133
+ 'fields_of' => {
134
+ signature: 'builtin fields_of(Type) -> array[field_handle, N]',
135
+ docs: '`fields_of(Type)` returns all struct fields as a compile-time `array[field_handle, N]`, in declaration order. Use with `inline for` for reflective field iteration.'
136
+ },
137
+ 'members_of' => {
138
+ signature: 'builtin members_of(Type) -> array[member_handle, N]',
139
+ docs: '`members_of(Type)` returns all members of an enum or flags type as a compile-time `array[member_handle, N]`. Each handle exposes `.name` and `.value`.'
140
+ },
141
+ 'attributes_of' => {
142
+ signature: 'builtin attributes_of(target [, name]) -> array[attribute_handle, N]',
143
+ docs: '`attributes_of(target)` returns all attributes applied to a type, field, or callable as a compile-time array. `attributes_of(target, name)` filters by attribute kind.'
144
+ },
133
145
  }.freeze
134
146
  OPERATOR_TOKEN_TYPES = %i[
135
147
  amp colon comma caret dot lparen rparen pipe lbracket rbracket question
@@ -158,9 +158,9 @@ module MilkTea
158
158
  ast = get_ast(uri)
159
159
  return @last_good_tooling_snapshot_cache[uri] if ast.nil?
160
160
 
161
- result = MilkTea::SemanticAnalyzer.check_collecting_errors(ast)
162
- facts = result[:analysis]
163
- MilkTea::SemanticAnalyzer::ToolingSnapshot.new(facts:, diagnostics: (Array(result[:errors]).map { |e| e.to_diagnostic(path: uri) } || []).freeze)
161
+ result = MilkTea::SemanticAnalyzer.check_collecting_errors(ast, path:)
162
+ facts = result[:analysis]
163
+ MilkTea::SemanticAnalyzer::ToolingSnapshot.new(facts:, diagnostics: (Array(result[:errors]).map { |e| e.to_diagnostic(path:) } || []).freeze)
164
164
  end
165
165
  if snapshot&.facts
166
166
  @last_good_tooling_snapshot_cache[uri] = snapshot
@@ -56,10 +56,12 @@ module MilkTea
56
56
  )
57
57
  collect_ms = elapsed_ms(collect_start) if collect_start
58
58
  diagnostics = result[:diagnostics]
59
+ cross_by_uri = result[:cross_file_diagnostics] || {}
59
60
  facts = result[:facts]
60
61
  snapshot = result[:sema_snapshot]
61
62
  @facts_cache_mutex.synchronize do
62
63
  if @facts_generation[uri] == generation
64
+ @cross_file_diagnostics = cross_by_uri
63
65
  @tooling_snapshot_cache[uri] = snapshot if snapshot
64
66
  @last_good_tooling_snapshot_cache[uri] = snapshot if snapshot&.facts
65
67
  @facts_cache[uri] = facts if facts
@@ -93,6 +95,10 @@ module MilkTea
93
95
  end
94
96
  end
95
97
  end
98
+
99
+ def cross_file_diagnostics
100
+ @facts_cache_mutex.synchronize { @cross_file_diagnostics }
101
+ end
96
102
  end
97
103
  end
98
104
  end
@@ -684,12 +684,17 @@ module MilkTea
684
684
  diagnostics = sort_by_location(diagnostics)
685
685
 
686
686
  if diagnostics.any? || closure_errors.any?
687
- source = read_source_file(path)
687
+ main_source = read_source_file(path)
688
+ main_abs = File.expand_path(path)
688
689
  diagnostics.each do |d|
690
+ same_file = !d.respond_to?(:path) || d.path.nil? || File.expand_path(d.path) == main_abs
691
+ source = same_file ? main_source : nil
689
692
  @err.puts(ErrorFormatter.format(d, source:, color: error_color?(@err)))
690
693
  end
691
694
  closure_errors.each do |d|
692
- @err.puts(ErrorFormatter.format(d, color: error_color?(@err)))
695
+ same_file = !d.respond_to?(:path) || d.path.nil? || File.expand_path(d.path) == main_abs
696
+ source = same_file ? main_source : nil
697
+ @err.puts(ErrorFormatter.format(d, source:, color: error_color?(@err)))
693
698
  end
694
699
  all_diagnostics.concat(diagnostics)
695
700
  all_diagnostics.concat(closure_errors)
@@ -711,9 +716,9 @@ module MilkTea
711
716
  parts << "#{info_count} #{info_count == 1 ? 'note' : 'notes'}" if info_count > 0
712
717
  body = parts.join("; ")
713
718
  if error_count > 0
714
- @err.puts("error: could not check due to #{body}")
719
+ @err.puts("#{body} found")
715
720
  elsif warning_count > 0
716
- @err.puts("warning: #{body}")
721
+ @err.puts("#{body}")
717
722
  end
718
723
  final_error_count = error_count + (resolution[:warnings_as_errors] ? warning_count : 0)
719
724
  final_error_count > 0 ? 1 : 0
@@ -586,6 +586,8 @@ module MilkTea
586
586
  return unless statement.type
587
587
  return unless statement.value
588
588
 
589
+ return if statement.type.nullable
590
+
589
591
  declared_name = type_ref_name(statement.type)
590
592
  return unless declared_name
591
593
 
@@ -666,6 +668,7 @@ module MilkTea
666
668
  # enum/int/str bool-matches are never misreported.
667
669
  def check_prefer_is_variant(match_expr)
668
670
  return unless @sema_facts
671
+ return if match_expr.desugared_from_is
669
672
 
670
673
  arms = match_expr.arms
671
674
  return unless arms.size == 2
@@ -771,6 +774,8 @@ module MilkTea
771
774
  return if stmts.any?(&:nil?)
772
775
  return unless stmts.all? { |s| inline_simple_statement?(s) }
773
776
 
777
+ return if visually_inline_if?(statement, stmts)
778
+
774
779
  emit_conciseness_hint(
775
780
  "prefer-inline-if",
776
781
  line: statement.line,
@@ -779,6 +784,15 @@ module MilkTea
779
784
  )
780
785
  end
781
786
 
787
+ def visually_inline_if?(statement, stmts)
788
+ n = statement.branches.length
789
+ statement.branches.each_with_index.all? { |b, i|
790
+ stmts[i].respond_to?(:line) && stmts[i].line == b.line
791
+ } && (
792
+ stmts[n].respond_to?(:line) && statement.else_line && stmts[n].line == statement.else_line
793
+ )
794
+ end
795
+
782
796
  # prefer-conditional-expression: if/match whose every branch either
783
797
  # returns a value or assigns the same lvalue can become an expression form
784
798
  # (`return if …: … else: …` or `x = match …`).
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.2.7
4
+ version: 0.2.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Long (Teefan) Tran
@@ -583,7 +583,7 @@ metadata:
583
583
  homepage_uri: https://teefan.github.io/mt-lang/
584
584
  source_code_uri: https://github.com/teefan/mt-lang
585
585
  post_install_message: |
586
- Milk Tea 0.2.7 installed!
586
+ Milk Tea 0.2.10 installed!
587
587
 
588
588
  System requirements:
589
589
  - A C compiler (gcc or clang) must be available on PATH