mt-lang 0.3.41 → 0.3.43

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -2
  3. data/docs/index.html +20 -4
  4. data/docs/language-design.md +5 -2
  5. data/docs/language-manual.md +28 -5
  6. data/lib/milk_tea/base.rb +1 -1
  7. data/lib/milk_tea/bindings/bindgen/declaration.rb +17 -0
  8. data/lib/milk_tea/bindings/bindgen/emitter.rb +44 -5
  9. data/lib/milk_tea/bindings/imported_bindings/defaults.rb +7 -0
  10. data/lib/milk_tea/bindings/raw_bindings/defaults.rb +22 -0
  11. data/lib/milk_tea/bindings/upstream_sources.rb +9 -0
  12. data/lib/milk_tea/bindings/vendored_box3d.rb +77 -0
  13. data/lib/milk_tea/bindings.rb +1 -0
  14. data/lib/milk_tea/core/c_backend/expressions.rb +21 -5
  15. data/lib/milk_tea/core/c_backend/feature_detection.rb +6 -1
  16. data/lib/milk_tea/core/c_backend/runtime_helpers.rb +38 -4
  17. data/lib/milk_tea/core/c_backend/statements.rb +1 -1
  18. data/lib/milk_tea/core/c_backend/type_collectors.rb +97 -4
  19. data/lib/milk_tea/core/c_backend.rb +9 -0
  20. data/lib/milk_tea/core/lowering/async/async_lowering.rb +2 -6
  21. data/lib/milk_tea/core/lowering/async/frame_builder.rb +12 -5
  22. data/lib/milk_tea/core/lowering/async/normalization.rb +28 -5
  23. data/lib/milk_tea/core/lowering/expressions.rb +31 -6
  24. data/lib/milk_tea/core/lowering/proc.rb +3 -0
  25. data/lib/milk_tea/core/lowering/resolve.rb +3 -0
  26. data/lib/milk_tea/core/lowering/utils.rb +26 -0
  27. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +21 -2
  28. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +49 -1
  29. data/lib/milk_tea/core/semantic_analyzer/statements.rb +1 -0
  30. data/lib/milk_tea/core/types/registry.rb +39 -16
  31. data/lib/milk_tea/lsp/server/hover.rb +0 -1
  32. data/lib/milk_tea/lsp/server/text_documents.rb +1 -1
  33. data/lib/milk_tea/tooling/docs_app.rb +1 -1
  34. data/std/box3d.mt +784 -0
  35. data/std/c/box3d.mt +1871 -0
  36. data/std/c/miniaudio.mt +19 -28
  37. data/std/c/steamworks.mt +2 -5
  38. data/std/miniaudio.mt +9 -12
  39. data/std/steamworks.mt +0 -1
  40. metadata +5 -2
@@ -51,6 +51,7 @@ module MilkTea
51
51
  "",
52
52
  "static mt_str mt_str_concat(mt_str a, mt_str b) {",
53
53
  "#{INDENT}uintptr_t total = a.len + b.len;",
54
+ "#{INDENT}if (total > MT_STR_CONCAT_BUF_SIZE) mt_fatal(\"str concatenation exceeds the scratch buffer budget\");",
54
55
  "#{INDENT}if (mt_str_concat_offset + total > MT_STR_CONCAT_BUF_SIZE) {",
55
56
  "#{INDENT * 2}mt_str_concat_offset = 0;",
56
57
  "#{INDENT}}",
@@ -63,6 +64,36 @@ module MilkTea
63
64
  ]
64
65
  end
65
66
 
67
+ def emit_str_slice_helpers
68
+ [
69
+ "static uint8_t mt_str_index(mt_str text, uintptr_t index) {",
70
+ "#{INDENT}if (index >= text.len) mt_fatal(\"str index out of bounds\");",
71
+ "#{INDENT}return (uint8_t)text.data[index];",
72
+ "}",
73
+ "",
74
+ "static bool mt_str_utf8_boundary(mt_str text, uintptr_t index) {",
75
+ "#{INDENT}if (index == 0 || index == text.len) return true;",
76
+ "#{INDENT}return ((uint8_t)text.data[index] & 0xC0) != 0x80;",
77
+ "}",
78
+ "",
79
+ "static mt_str mt_str_slice(mt_str text, uintptr_t start, uintptr_t stop) {",
80
+ "#{INDENT}if (start > stop || stop > text.len) mt_fatal(\"str slice out of bounds\");",
81
+ "#{INDENT}if (!mt_str_utf8_boundary(text, start) || !mt_str_utf8_boundary(text, stop)) mt_fatal(\"str slice bounds must be UTF-8 code-unit boundaries\");",
82
+ "#{INDENT}return (mt_str){ .data = text.data + start, .len = stop - start };",
83
+ "}",
84
+ ]
85
+ end
86
+
87
+ def emit_span_slice_helper(helper_name)
88
+ span_name = helper_name.sub("mt_span_slice_", "mt_span_")
89
+ [
90
+ "static inline #{span_name} #{helper_name}(#{span_name} span, uintptr_t start, uintptr_t stop) {",
91
+ "#{INDENT}if (start > stop || stop > span.len) mt_fatal(\"span slice out of bounds\");",
92
+ "#{INDENT}return (#{span_name}){ .data = span.data + start, .len = stop - start };",
93
+ "}",
94
+ ]
95
+ end
96
+
66
97
  def emit_variant_equality_helpers
67
98
  variant_decls_by_linkage = (emitted_aggregate_variants + collect_generic_variant_decls).each_with_object({}) { |decl, map| map[decl.linkage_name] = decl }
68
99
  variant_equality_types
@@ -297,6 +328,9 @@ module MilkTea
297
328
  "#{INDENT}for (int t = 0; t < nworkers; t++) {",
298
329
  "#{INDENT * 2}uv_thread_join(&threads[t]);",
299
330
  "#{INDENT}}",
331
+ "#{INDENT}for (int t = nworkers + 1; t < count; t++) {",
332
+ "#{INDENT * 2}items[t].work(items[t].data);",
333
+ "#{INDENT}}",
300
334
  "}",
301
335
  ]
302
336
  end
@@ -590,11 +624,11 @@ module MilkTea
590
624
 
591
625
  def emit_checked_array_index_helper(type)
592
626
  helper_name = checked_array_index_helper_name(type)
593
- params = [c_declaration(type, '(*array)'), c_declaration(Types::Registry.primitive('ptr_uint'), 'index')].join(', ')
627
+ params = [c_declaration(pointer_to(array_element_type(type)), 'array'), c_declaration(Types::Registry.primitive('ptr_uint'), 'index')].join(', ')
594
628
  [
595
629
  "static inline #{c_function_declaration(pointer_to(array_element_type(type)), helper_name, params)} {",
596
630
  "#{INDENT}if (index >= #{array_length(type)}) mt_fatal(\"array index out of bounds\");",
597
- "#{INDENT}return &(*array)[index];",
631
+ "#{INDENT}return &array[index];",
598
632
  "}",
599
633
  ]
600
634
  end
@@ -612,11 +646,11 @@ module MilkTea
612
646
 
613
647
  def emit_nullable_array_index_helper(type)
614
648
  helper_name = nullable_array_index_helper_name(type)
615
- params = [c_declaration(type, '(*array)'), c_declaration(Types::Registry.primitive('ptr_uint'), 'index')].join(', ')
649
+ params = [c_declaration(pointer_to(array_element_type(type)), 'array'), c_declaration(Types::Registry.primitive('ptr_uint'), 'index')].join(', ')
616
650
  [
617
651
  "static inline #{c_function_declaration(pointer_to(array_element_type(type)), helper_name, params)} {",
618
652
  "#{INDENT}if (index >= #{array_length(type)}) return NULL;",
619
- "#{INDENT}return &(*array)[index];",
653
+ "#{INDENT}return &array[index];",
620
654
  "}",
621
655
  ]
622
656
  end
@@ -495,7 +495,7 @@ module MilkTea
495
495
  def emit_checked_index_pointer(expression)
496
496
  case expression
497
497
  when IR::CheckedIndex
498
- "#{checked_array_index_helper_name(expression.receiver_type)}(#{emit_address_of_operand(expression.receiver)}, #{emit_expression(expression.index)})"
498
+ "#{checked_array_index_helper_name(expression.receiver_type)}(#{emit_checked_array_index_argument(expression.receiver)}, #{emit_expression(expression.index)})"
499
499
  when IR::CheckedSpanIndex
500
500
  "#{checked_span_index_helper_name(expression.receiver_type)}(#{emit_expression(expression.receiver)}, #{emit_expression(expression.index)})"
501
501
  else
@@ -19,6 +19,80 @@ module MilkTea
19
19
  span_types.uniq
20
20
  end
21
21
 
22
+ def collect_span_slice_helper_names
23
+ names = []
24
+ emitted_functions.each do |function|
25
+ collect_span_slice_helper_names_from_statements(function.body, names)
26
+ end
27
+ names.uniq
28
+ end
29
+
30
+ def collect_span_slice_helper_names_from_statements(statements, names)
31
+ statements.each do |statement|
32
+ case statement
33
+ when IR::LocalDecl
34
+ collect_span_slice_helper_names_from_expression(statement.value, names)
35
+ when IR::Assignment
36
+ collect_span_slice_helper_names_from_expression(statement.target, names)
37
+ collect_span_slice_helper_names_from_expression(statement.value, names)
38
+ when IR::BlockStmt
39
+ collect_span_slice_helper_names_from_statements(statement.body, names)
40
+ when IR::WhileStmt
41
+ collect_span_slice_helper_names_from_expression(statement.condition, names)
42
+ collect_span_slice_helper_names_from_statements(statement.body, names)
43
+ when IR::ForStmt
44
+ collect_span_slice_helper_names_from_statements([statement.init], names)
45
+ collect_span_slice_helper_names_from_expression(statement.condition, names)
46
+ collect_span_slice_helper_names_from_statements(statement.body, names)
47
+ collect_span_slice_helper_names_from_statements([statement.post], names)
48
+ when IR::IfStmt
49
+ collect_span_slice_helper_names_from_expression(statement.condition, names)
50
+ collect_span_slice_helper_names_from_statements(statement.then_body, names)
51
+ collect_span_slice_helper_names_from_statements(statement.else_body, names) if statement.else_body
52
+ when IR::SwitchStmt
53
+ collect_span_slice_helper_names_from_expression(statement.expression, names)
54
+ statement.cases.each { |switch_case| collect_span_slice_helper_names_from_statements(switch_case.body, names) }
55
+ when IR::ReturnStmt
56
+ collect_span_slice_helper_names_from_expression(statement.value, names) if statement.value
57
+ when IR::ExpressionStmt
58
+ collect_span_slice_helper_names_from_expression(statement.expression, names)
59
+ end
60
+ end
61
+ end
62
+
63
+ def collect_span_slice_helper_names_from_expression(expression, names)
64
+ return unless expression
65
+
66
+ case expression
67
+ when IR::Call
68
+ names << expression.callee if expression.callee.is_a?(String) && expression.callee.start_with?("mt_span_slice_")
69
+ collect_span_slice_helper_names_from_expression(expression.callee, names) unless expression.callee.is_a?(String)
70
+ expression.arguments.each { |argument| collect_span_slice_helper_names_from_expression(argument, names) }
71
+ when IR::Member
72
+ collect_span_slice_helper_names_from_expression(expression.receiver, names)
73
+ when IR::Index, IR::CheckedIndex, IR::CheckedSpanIndex, IR::NullableIndex, IR::NullableSpanIndex
74
+ collect_span_slice_helper_names_from_expression(expression.receiver, names)
75
+ collect_span_slice_helper_names_from_expression(expression.index, names)
76
+ when IR::Unary
77
+ collect_span_slice_helper_names_from_expression(expression.operand, names)
78
+ when IR::Binary
79
+ collect_span_slice_helper_names_from_expression(expression.left, names)
80
+ collect_span_slice_helper_names_from_expression(expression.right, names)
81
+ when IR::Conditional
82
+ collect_span_slice_helper_names_from_expression(expression.condition, names)
83
+ collect_span_slice_helper_names_from_expression(expression.then_expression, names)
84
+ collect_span_slice_helper_names_from_expression(expression.else_expression, names)
85
+ when IR::Cast, IR::AddressOf
86
+ collect_span_slice_helper_names_from_expression(expression.expression, names)
87
+ when IR::AggregateLiteral
88
+ expression.fields.each { |field| collect_span_slice_helper_names_from_expression(field.value, names) }
89
+ when IR::ArrayLiteral
90
+ expression.elements.each { |element| collect_span_slice_helper_names_from_expression(element, names) }
91
+ when IR::VariantLiteral
92
+ expression.fields.each { |field| collect_span_slice_helper_names_from_expression(field.value, names) }
93
+ end
94
+ end
95
+
22
96
  def collect_checked_array_index_types_from_statements(statements, array_types, nullable_only: false)
23
97
  statements.each do |statement|
24
98
  case statement
@@ -223,6 +297,26 @@ module MilkTea
223
297
  soa_types = []
224
298
  visited = {}
225
299
 
300
+ all_emitted_top_level_values.each do |value|
301
+ collect_soa_type(value.type, soa_types, visited)
302
+ end
303
+
304
+ @program.structs.each do |struct_decl|
305
+ struct_decl.fields.each do |field|
306
+ collect_soa_type(field.type, soa_types, visited)
307
+ end
308
+ end
309
+
310
+ @program.unions.each do |union_decl|
311
+ union_decl.fields.each do |field|
312
+ collect_soa_type(field.type, soa_types, visited)
313
+ end
314
+ end
315
+
316
+ each_variant_arm_field_type do |field_type|
317
+ collect_soa_type(field_type, soa_types, visited)
318
+ end
319
+
226
320
  emitted_functions.each do |function|
227
321
  collect_soa_type(function.return_type, soa_types, visited)
228
322
  function.params.each do |param|
@@ -231,10 +325,9 @@ module MilkTea
231
325
  collect_soa_from_statements(function.body, soa_types, visited)
232
326
  end
233
327
 
234
- @program.structs.each do |struct_decl|
235
- struct_decl.fields.each do |field|
236
- collect_soa_type(field.type, soa_types, visited)
237
- end
328
+ @program.static_asserts.each do |statement|
329
+ collect_soa_type_in_expression(statement.condition, soa_types, visited)
330
+ collect_soa_type_in_expression(statement.message, soa_types, visited)
238
331
  end
239
332
 
240
333
  soa_types.uniq
@@ -162,6 +162,10 @@ module MilkTea
162
162
  lines.concat(emit_str_concat_helper)
163
163
  lines << ""
164
164
  end
165
+ if uses_str_slice_helpers?
166
+ lines.concat(emit_str_slice_helpers)
167
+ lines << ""
168
+ end
165
169
  if uses_str_buffer_helpers?
166
170
  lines.concat(emit_utf8_validation_helpers)
167
171
  lines << ""
@@ -335,6 +339,11 @@ module MilkTea
335
339
  lines << ""
336
340
  end
337
341
 
342
+ collect_span_slice_helper_names.each do |helper_name|
343
+ lines.concat(emit_span_slice_helper(helper_name))
344
+ lines << ""
345
+ end
346
+
338
347
  collect_checked_array_index_types(nullable_only: true).each do |type|
339
348
  lines.concat(emit_nullable_array_index_helper(type))
340
349
  lines << ""
@@ -636,13 +636,9 @@ module MilkTea
636
636
  active_defers: active_defers + local_defers,
637
637
  loop_flow: nested_loop_flow(loop_flow, local_defers),
638
638
  )
639
+ storage_ref = IR::Name.new(name: linkage_name, type: storage_type, pointer: false)
639
640
  lowered << IR::IfStmt.new(
640
- condition: IR::Binary.new(
641
- operator: "==",
642
- left: IR::Name.new(name: linkage_name, type: storage_type, pointer: false),
643
- right: IR::NullLiteral.new(type: storage_type),
644
- type: @ctx.types.fetch("bool"),
645
- ),
641
+ condition: let_else_failure_condition(storage_ref, storage_type),
646
642
  then_body: else_body,
647
643
  else_body: nil,
648
644
  )
@@ -421,7 +421,8 @@ module MilkTea
421
421
  body = [async_frame_cast_declaration(frame_type, async_info)]
422
422
 
423
423
  not_ready_expr = IR::Unary.new(operator: "not", operand: async_frame_field_expression(frame_expr, "ready", @ctx.types.fetch("bool")), type: @ctx.types.fetch("bool"))
424
- not_ready_return = [IR::ReturnStmt.new(value: nil)]
424
+ frame_free_stmt = IR::ExpressionStmt.new(expression: IR::Call.new(callee: "mt_async_free", arguments: [raw_frame_expr], type: @ctx.types.fetch("void")))
425
+ not_ready_return = [frame_free_stmt, IR::ReturnStmt.new(value: nil)]
425
426
 
426
427
  if async_info[:await_fields].any?
427
428
  await_release_stmts = []
@@ -589,11 +590,17 @@ module MilkTea
589
590
  )
590
591
  body << IR::ExpressionStmt.new(expression: ready_assign)
591
592
 
592
- waiter_frame_expr = async_frame_field_expression(frame_expr, "waiter_frame", async_info[:void_ptr])
593
+ waiter_frame_field = async_frame_field_expression(frame_expr, "waiter_frame", async_info[:void_ptr])
593
594
  wake_stmts = [
595
+ IR::LocalDecl.new(
596
+ name: "waiter_frame",
597
+ linkage_name: "__mt_waiter_frame",
598
+ type: async_info[:void_ptr],
599
+ value: waiter_frame_field,
600
+ ),
594
601
  IR::ExpressionStmt.new(
595
602
  expression: IR::Assignment.new(
596
- target: async_frame_field_expression(frame_expr, "waiter_frame", async_info[:void_ptr]),
603
+ target: waiter_frame_field,
597
604
  operator: "=",
598
605
  value: IR::NullLiteral.new(type: async_info[:void_ptr]),
599
606
  ),
@@ -601,13 +608,13 @@ module MilkTea
601
608
  IR::ExpressionStmt.new(
602
609
  expression: IR::Call.new(
603
610
  callee: IR::Name.new(name: async_frame_field_c_name("waiter"), type: async_info[:wake_type], pointer: false),
604
- arguments: [waiter_frame_expr],
611
+ arguments: [IR::Name.new(name: "__mt_waiter_frame", type: async_info[:void_ptr], pointer: false)],
605
612
  type: @ctx.types.fetch("void"),
606
613
  ),
607
614
  ),
608
615
  ]
609
616
  body << IR::IfStmt.new(
610
- condition: waiter_frame_expr,
617
+ condition: waiter_frame_field,
611
618
  then_body: wake_stmts,
612
619
  else_body: nil,
613
620
  )
@@ -364,6 +364,17 @@ module MilkTea
364
364
  end
365
365
  end
366
366
 
367
+ # Emits a type-name prefix that the semantic re-check of generated proc
368
+ # roots can actually resolve: bare names for prelude and current-module
369
+ # types, the qualified module path otherwise.
370
+ def ast_type_ref_base_parts(type)
371
+ return [type.name] if type.module_name.nil?
372
+ return [type.name] if type.module_name == @ctx.module_name
373
+ return [type.name] if %w[std.option std.result].include?(type.module_name)
374
+
375
+ type.module_name.split(".") + [type.name]
376
+ end
377
+
367
378
  def ast_type_ref_for(type)
368
379
  case type
369
380
  when Types::Primitive
@@ -389,12 +400,25 @@ module MilkTea
389
400
  AST::TypeRef.new(name: AST::QualifiedName.new(parts: ["span"]), arguments: [AST::TypeArgument.new(value: ast_type_ref_for(type.element_type))], nullable: false)
390
401
  when Types::Task
391
402
  AST::TypeRef.new(name: AST::QualifiedName.new(parts: ["Task"]), arguments: [AST::TypeArgument.new(value: ast_type_ref_for(type.result_type))], nullable: false)
403
+ when Types::StringView
404
+ AST::TypeRef.new(name: AST::QualifiedName.new(parts: ["str"]), arguments: [], nullable: false)
392
405
  when Types::TypeVar
393
406
  AST::TypeRef.new(name: AST::QualifiedName.new(parts: [type.name]), arguments: [], nullable: false)
394
407
  when Types::StructInstance
395
- base_parts = type.module_name ? type.module_name.split(".") + [type.name] : [type.name]
396
408
  AST::TypeRef.new(
397
- name: AST::QualifiedName.new(parts: base_parts),
409
+ name: AST::QualifiedName.new(parts: ast_type_ref_base_parts(type)),
410
+ arguments: type.arguments.map do |argument|
411
+ if argument.is_a?(Types::LiteralTypeArg)
412
+ AST::TypeArgument.new(value: AST::IntegerLiteral.new(lexeme: argument.value.to_s, value: argument.value))
413
+ else
414
+ AST::TypeArgument.new(value: ast_type_ref_for(argument))
415
+ end
416
+ end,
417
+ nullable: false,
418
+ )
419
+ when Types::VariantInstance
420
+ AST::TypeRef.new(
421
+ name: AST::QualifiedName.new(parts: ast_type_ref_base_parts(type)),
398
422
  arguments: type.arguments.map do |argument|
399
423
  if argument.is_a?(Types::LiteralTypeArg)
400
424
  AST::TypeArgument.new(value: AST::IntegerLiteral.new(lexeme: argument.value.to_s, value: argument.value))
@@ -404,9 +428,8 @@ module MilkTea
404
428
  end,
405
429
  nullable: false,
406
430
  )
407
- when Types::Struct, Types::Union, Types::Opaque, Types::Enum, Types::Flags
408
- parts = type.module_name ? type.module_name.split(".") + [type.name] : [type.name]
409
- AST::TypeRef.new(name: AST::QualifiedName.new(parts: parts), arguments: [], nullable: false)
431
+ when Types::Struct, Types::Union, Types::Opaque, Types::Enum, Types::Flags, Types::Variant
432
+ AST::TypeRef.new(name: AST::QualifiedName.new(parts: ast_type_ref_base_parts(type)), arguments: [], nullable: false)
410
433
  when Types::Function
411
434
  AST::FunctionType.new(
412
435
  params: type.params.each_with_index.map { |param, i| AST::Param.new(name: param.name || "p#{i}", type: ast_type_ref_for(param.type)) },
@@ -1364,13 +1364,19 @@ module MilkTea
1364
1364
  when AST::IndexAccess
1365
1365
  receiver_type = infer_expression_type(expression.receiver, env:)
1366
1366
  receiver = lower_expression(expression.receiver, env:)
1367
- index = lower_expression(expression.index, env:)
1368
- if array_type?(receiver_type) && addressable_storage_expression?(expression.receiver)
1369
- IR::CheckedIndex.new(receiver:, index:, receiver_type:, type:)
1370
- elsif receiver_type.is_a?(Types::Span)
1371
- IR::CheckedSpanIndex.new(receiver:, index:, receiver_type:, type:)
1367
+ if expression.index.is_a?(AST::RangeExpr)
1368
+ lower_range_index_access(receiver, receiver_type, expression.index, env:, type:)
1372
1369
  else
1373
- IR::Index.new(receiver:, index:, type:)
1370
+ index = lower_expression(expression.index, env:)
1371
+ if array_type?(receiver_type) && addressable_storage_expression?(expression.receiver)
1372
+ IR::CheckedIndex.new(receiver:, index:, receiver_type:, type:)
1373
+ elsif receiver_type.is_a?(Types::Span)
1374
+ IR::CheckedSpanIndex.new(receiver:, index:, receiver_type:, type:)
1375
+ elsif receiver_type.is_a?(Types::StringView)
1376
+ IR::Call.new(callee: "mt_str_index", arguments: [receiver, index], type:)
1377
+ else
1378
+ IR::Index.new(receiver:, index:, type:)
1379
+ end
1374
1380
  end
1375
1381
  when AST::UnaryOp
1376
1382
  raise LoweringError.new("propagation expressions must be prepared before direct lowering", line: 0, column: 0, path: @ctx.current_analysis_path) if expression.operator == "?"
@@ -1452,6 +1458,25 @@ module MilkTea
1452
1458
  end
1453
1459
  end
1454
1460
 
1461
+ def lower_range_index_access(receiver, receiver_type, range, env:, type:)
1462
+ start = lower_range_index_bound(range.start_expr, env:)
1463
+ stop = lower_range_index_bound(range.end_expr, env:)
1464
+
1465
+ if receiver_type.is_a?(Types::StringView)
1466
+ return IR::Call.new(callee: "mt_str_slice", arguments: [receiver, start, stop], type:)
1467
+ end
1468
+
1469
+ span_type = range_index_result_type(receiver_type)
1470
+ span_argument = receiver_type.is_a?(Types::Span) ? receiver : lower_array_to_span_expression(receiver, span_type)
1471
+ IR::Call.new(callee: span_slice_callee(span_type), arguments: [span_argument, start, stop], type: span_type)
1472
+ end
1473
+
1474
+ def lower_range_index_bound(bound, env:)
1475
+ ptr_uint = @ctx.types.fetch("ptr_uint")
1476
+ lowered = lower_expression(bound, env:, expected_type: ptr_uint)
1477
+ lowered.type == ptr_uint ? lowered : IR::Cast.new(target_type: ptr_uint, expression: lowered, type: ptr_uint)
1478
+ end
1479
+
1455
1480
  def lower_member_access(expression, env:, type:)
1456
1481
  if (type_expr = resolve_type_expression(expression.receiver))
1457
1482
  if type_expr.is_a?(Types::Variant)
@@ -206,6 +206,9 @@ module MilkTea
206
206
  when AST::IndexAccess
207
207
  collect_proc_captures_from_expression(expression.receiver, env, local_scopes, captures)
208
208
  collect_proc_captures_from_expression(expression.index, env, local_scopes, captures)
209
+ when AST::RangeExpr
210
+ collect_proc_captures_from_expression(expression.start_expr, env, local_scopes, captures)
211
+ collect_proc_captures_from_expression(expression.end_expr, env, local_scopes, captures)
209
212
  when AST::Specialization
210
213
  collect_proc_captures_from_expression(expression.callee, env, local_scopes, captures)
211
214
  expression.arguments.each { |argument| collect_proc_captures_from_expression(argument.value, env, local_scopes, captures) }
@@ -786,6 +786,9 @@ module MilkTea
786
786
  raise LoweringError.new("unknown member #{expression.member}", line: expression.line, column: expression.column, path: @ctx.current_analysis_path)
787
787
  when AST::IndexAccess
788
788
  receiver_type = infer_expression_type(expression.receiver, env:)
789
+ if expression.index.is_a?(AST::RangeExpr)
790
+ return range_index_result_type(receiver_type)
791
+ end
789
792
  index_type = infer_expression_type(expression.index, env:)
790
793
  infer_index_result_type(receiver_type, index_type)
791
794
  when AST::UnaryOp
@@ -213,6 +213,10 @@ module MilkTea
213
213
 
214
214
  receiver_type = referenced_type(receiver_type) if ref_type?(receiver_type)
215
215
 
216
+ if receiver_type.is_a?(Types::StringView)
217
+ return @ctx.types.fetch("ubyte")
218
+ end
219
+
216
220
  if array_type?(receiver_type)
217
221
  return array_element_type(receiver_type)
218
222
  end
@@ -236,6 +240,28 @@ module MilkTea
236
240
  raise LoweringError.new("cannot index #{receiver_type}", line: 0, column: 0, path: @ctx.current_analysis_path)
237
241
  end
238
242
 
243
+ def range_index_result_type(receiver_type)
244
+ receiver_type = referenced_type(receiver_type) if ref_type?(receiver_type)
245
+
246
+ if receiver_type.is_a?(Types::StringView)
247
+ return @ctx.types.fetch("str")
248
+ end
249
+
250
+ if array_type?(receiver_type)
251
+ return Types::Span.new(array_element_type(receiver_type))
252
+ end
253
+
254
+ if receiver_type.is_a?(Types::Span)
255
+ return receiver_type
256
+ end
257
+
258
+ raise LoweringError.new("cannot range-index #{receiver_type}; expected str, array[T, N], or span[T]", line: 0, column: 0, path: @ctx.current_analysis_path)
259
+ end
260
+
261
+ def span_slice_callee(span_type)
262
+ "mt_span_slice_#{sanitize_identifier(span_type.element_type.to_s)}"
263
+ end
264
+
239
265
  def stored_ref_supported_type?(type, visited = {})
240
266
  return true unless type
241
267
 
@@ -37,6 +37,8 @@ module MilkTea
37
37
  allow_span_param_identifier: true,
38
38
  )
39
39
 
40
+ raise_sema_error("cannot assign through str index; str is an immutable borrowed view") if receiver_type.is_a?(Types::StringView)
41
+
40
42
  index_type = infer_expression(expression.index, scopes:)
41
43
  infer_index_result_type(receiver_type, index_type)
42
44
  when AST::Call
@@ -101,6 +103,8 @@ module MilkTea
101
103
  require_mutable_pointer:,
102
104
  allow_span_param_identifier:,
103
105
  )
106
+ raise_sema_error("cannot index through str as an assignment receiver; str is an immutable borrowed view") if receiver_type.is_a?(Types::StringView)
107
+
104
108
  index_type = infer_expression(expression.index, scopes:)
105
109
  infer_index_result_type(receiver_type, index_type)
106
110
  when AST::Call
@@ -248,7 +252,13 @@ module MilkTea
248
252
  raise_sema_error("unsupported expression #{expression.class.name}")
249
253
  end
250
254
 
251
- @resolved_expr_types[@ctx.ast.node_ids[expression.object_id]] = type
255
+ # Specialized generic-instance bodies share the same AST nodes across
256
+ # substitutions, so a node-id keyed cache cannot be populated there:
257
+ # the last-checked instance would overwrite every other instance's
258
+ # types and leak wrong types to lowering, which reads this cache.
259
+ # Regular functions carry an empty (but non-nil) substitution hash, so
260
+ # only a non-empty hash identifies an instance body.
261
+ @resolved_expr_types[@ctx.ast.node_ids[expression.object_id]] = type unless @current_type_substitutions&.any?
252
262
  type
253
263
  end
254
264
  end
@@ -464,8 +474,17 @@ module MilkTea
464
474
 
465
475
  def infer_index_access(expression, scopes:)
466
476
  receiver_type = infer_expression(expression.receiver, scopes:)
477
+
478
+ if expression.index.is_a?(AST::RangeExpr)
479
+ return infer_range_index_access(expression, receiver_type, scopes:)
480
+ end
481
+
467
482
  index_type = infer_expression(expression.index, scopes:)
468
483
 
484
+ if receiver_type.is_a?(Types::StringView) && (literal = integer_literal_bound_value(expression.index)) && literal < 0
485
+ raise_sema_error("str index #{literal} is negative; str indices must be non-negative", expression)
486
+ end
487
+
469
488
  if soa_type?(receiver_type)
470
489
  return receiver_type.element_type
471
490
  end
@@ -961,7 +980,7 @@ module MilkTea
961
980
  callable_kind = resolution.kind
962
981
  callable = resolution.value
963
982
  receiver = resolution.receiver
964
- @resolved_call_kinds[@ctx.ast.node_ids[expression.callee.object_id]] = callable_kind
983
+ @resolved_call_kinds[@ctx.ast.node_ids[expression.callee.object_id]] = callable_kind unless @current_type_substitutions&.any?
965
984
 
966
985
  case callable_kind
967
986
  when :function
@@ -695,7 +695,9 @@ module MilkTea
695
695
  offset = CompileTime::Layout.offset_of(type, binding.const_value.field_name)
696
696
  return unless offset
697
697
 
698
- @const_values[@ctx.ast.node_ids[expression.object_id]] = offset
698
+ # Same node-id key collision as resolved_expr_types: instance bodies
699
+ # share AST nodes across substitutions, so never cache their values.
700
+ @const_values[@ctx.ast.node_ids[expression.object_id]] = offset unless @current_type_substitutions&.any?
699
701
  end
700
702
 
701
703
  def infer_offsetof_type(type_ref, field_name, scopes: nil)
@@ -1020,6 +1022,10 @@ module MilkTea
1020
1022
  def infer_index_result_type(receiver_type, index_type)
1021
1023
  raise_sema_error("index must be an integer type, got #{index_type}") unless integer_type?(index_type)
1022
1024
 
1025
+ if receiver_type.is_a?(Types::StringView)
1026
+ return @ctx.types.fetch("ubyte")
1027
+ end
1028
+
1023
1029
  if array_type?(receiver_type)
1024
1030
  return array_element_type(receiver_type)
1025
1031
  end
@@ -1045,6 +1051,48 @@ module MilkTea
1045
1051
  raise_sema_error("cannot index #{receiver_type}")
1046
1052
  end
1047
1053
 
1054
+ def infer_range_index_access(expression, receiver_type, scopes:)
1055
+ start_type = infer_expression(expression.index.start_expr, scopes:)
1056
+ stop_type = infer_expression(expression.index.end_expr, scopes:)
1057
+ raise_sema_error("range index bounds must be integer types, got #{start_type} and #{stop_type}") unless start_type.integer? && stop_type.integer?
1058
+
1059
+ if array_type?(receiver_type) && !addressable_storage_expression?(expression.receiver, scopes:)
1060
+ raise_sema_error("array range slice requires an addressable array value; bind it to a local first", expression)
1061
+ end
1062
+
1063
+ if array_type?(receiver_type) && (start_val = integer_literal_bound_value(expression.index.start_expr)) && (stop_val = integer_literal_bound_value(expression.index.end_expr))
1064
+ length = array_length(receiver_type)
1065
+ unless start_val >= 0 && start_val <= stop_val && stop_val <= length
1066
+ raise_sema_error("range index [#{start_val}..#{stop_val}] is out of bounds for array[T, #{length}]", expression)
1067
+ end
1068
+ end
1069
+
1070
+ range_index_result_type(receiver_type)
1071
+ end
1072
+
1073
+ def range_index_result_type(receiver_type)
1074
+ if receiver_type.is_a?(Types::StringView)
1075
+ return @ctx.types.fetch("str")
1076
+ end
1077
+
1078
+ if array_type?(receiver_type)
1079
+ return Types::Span.new(array_element_type(receiver_type))
1080
+ end
1081
+
1082
+ if span_type?(receiver_type)
1083
+ return receiver_type
1084
+ end
1085
+
1086
+ raise_sema_error("cannot range-index #{receiver_type}; expected str, array[T, N], or span[T]")
1087
+ end
1088
+
1089
+ def integer_literal_bound_value(expression)
1090
+ return expression.value if expression.is_a?(AST::IntegerLiteral)
1091
+ return -expression.operand.value if expression.is_a?(AST::UnaryOp) && expression.operator == "-" && expression.operand.is_a?(AST::IntegerLiteral)
1092
+
1093
+ nil
1094
+ end
1095
+
1048
1096
  def addressable_storage_expression?(expression, scopes:)
1049
1097
  case expression
1050
1098
  when AST::Identifier
@@ -538,6 +538,7 @@ module MilkTea
538
538
  require_mutable_pointer: true,
539
539
  allow_span_param_identifier: true,
540
540
  )
541
+ raise_sema_error("cannot assign through str index; str is an immutable borrowed view") if receiver_type.is_a?(Types::StringView)
541
542
  element_type = infer_index_result_type(receiver_type, @ctx.types.fetch("ptr_uint"))
542
543
 
543
544
  statement.value.elements.each_with_index do |elem, i|