STUDIO HAZE · topaz
Topaz source
Open by house policy. Topaz is our own language.
Topaz 5.20 · 56 files
compiler/crates/topaz_self_frontend/topaz/checker/expression_control.tpz
Topaz · 47054 B · Original file
1
// Inference for operators, branches, loops, matches, and concurrent forms.2
// Core literals and member access stay in expression_core; recursive descent is3
// routed through the service boundary owned by the checker facade.5
import types { SyntaxNode }6
import checker_types { TypeValue }7
import checker.model {8
CallPreparation,9
CheckerService,10
CheckerState,11
LeadingCallArgument,12
LoopFrame,13
MAX_DURATION_MILLISECONDS,14
MAX_DURATION_MINUTES,15
MAX_DURATION_SECONDS,16
constructor,17
emptyType,18
functionType,19
literal,20
nominal,21
primitive,22
serviceCheckBlock,23
serviceCheckBlockExpected,24
structural,25
}26
import checker.type_algebra {27
assignable,28
hasHole,29
hasPartialHole,30
joinedTypes,31
solvePartialTypeAgainst,32
stringJoin,33
typeText,34
unionType,35
widen,36
widenedUnion,37
}38
import checker.syntax_index {39
addDiagnostic,40
addNode,41
attribute,42
child,43
children,44
text,45
}46
import checker.members_capabilities {47
comparableType,48
gateCollectionKey,49
memberType,50
orderCapability,51
}52
import checker.patterns {53
coverageGap,54
emptyPatternCoverage,55
mergePatternCoverage,56
patternBindingType,57
patternBindingTypeAt,58
}59
import checker.call_facts {60
addPipeCall,61
argumentValue,62
checkExpressionExpected,63
checkReturn,64
containsPlaceholder,65
currentExpectedExpression,66
currentFunctionReturn,67
currentPipeValue,68
inferExpressionWithExpected,69
recordInferredExpression,70
refineExpressionToExpected,71
reportExpectedType,72
}73
import checker.expression_core {74
blockTerminates,75
checkedIterableElement,76
expressionTerminates,77
hasBranchJoinContext,78
hasLiteralJoinContext,79
inferBranchExpression,80
inferIdentifierExpression,81
inferLambda,82
inferMapLiteralExpression,83
inferMemberAccessExpression,84
inferRecordLiteralExpression,85
inferRecordUpdateExpression,86
inferSequenceLiteralExpression,87
inferTextExpression,88
}89
import checker.call_complete {90
callArgumentInferenceContext,91
completePreparedCall,92
inferCall,93
inferCallArguments,94
inferContextualCallArgument,95
leadingCallActuals,96
prepareCall,97
}99
function inferIfExpression(100
service: CheckerService,101
state: CheckerState,102
moduleIndex: int,103
source: Bytes,104
node: SyntaxNode,105
) -> TypeValue {106
let contextual = hasLiteralJoinContext(state, node)107
let expected = currentExpectedExpression(state, node)108
match child(node, "condition") {109
case Some(found) => {110
let _ = checkExpressionExpected(111
service,112
state,113
moduleIndex,114
source,115
found,116
primitive("bool"),117
)118
}119
case None => ()120
}121
let mut branches: Array<TypeValue> = []122
match child(node, "then") {123
case Some(found) => {124
let terminating = blockTerminates(found)125
let branch = if terminating {126
serviceCheckBlock(127
service,128
state,129
moduleIndex,130
source,131
found,132
)133
} else {134
match expected {135
case Some(expectedType) => if hasHole(expectedType) {136
inferBranchExpression(137
service,138
state,139
moduleIndex,140
source,141
found,142
contextual,143
)144
} else {145
serviceCheckBlockExpected(146
service,147
state,148
moduleIndex,149
source,150
found,151
expectedType,152
)153
}154
case None => inferBranchExpression(155
service,156
state,157
moduleIndex,158
source,159
found,160
contextual,161
)162
}163
}164
if !terminating {165
branches.push(branch)166
}167
}168
case None => ()169
}170
match child(node, "else") {171
case Some(found) => {172
let terminating = expressionTerminates(found)173
let branch = if terminating {174
inferExpression(service, state, moduleIndex, source, found)175
} else {176
match expected {177
case Some(expectedType) => if hasHole(expectedType) {178
inferBranchExpression(179
service,180
state,181
moduleIndex,182
source,183
found,184
contextual,185
)186
} else {187
checkExpressionExpected(188
service,189
state,190
moduleIndex,191
source,192
found,193
expectedType,194
)195
}196
case None => inferBranchExpression(197
service,198
state,199
moduleIndex,200
source,201
found,202
contextual,203
)204
}205
}206
if !terminating {207
branches.push(branch)208
}209
}210
case None => branches.push(primitive("unit"))211
}212
let expectedContext = match expected {213
case Some(found) => !hasHole(found)214
case None => false215
}216
joinedTypes(state, branches, !(contextual || expectedContext))217
}219
function inferMatchExpression(220
service: CheckerService,221
state: CheckerState,222
moduleIndex: int,223
source: Bytes,224
node: SyntaxNode,225
) -> TypeValue {226
let contextual = hasLiteralJoinContext(state, node)227
let scrutinee = match child(node, "scrutinee") {228
case Some(found) => inferExpression(service, state, moduleIndex, source, found)229
case None => emptyType("unknown")230
}231
let mut branches: Array<TypeValue> = []232
let mut coverage = emptyPatternCoverage()233
for foundCase in children(node, "cases") {234
let mut caseCoverage = emptyPatternCoverage()235
match child(foundCase, "pattern") {236
case Some(pattern) => {237
caseCoverage = patternBindingTypeAt(238
service,239
state,240
moduleIndex,241
source,242
pattern,243
scrutinee,244
false,245
true,246
)247
}248
case None => ()249
}250
match child(foundCase, "guard") {251
case Some(guard) => {252
let _ = checkExpressionExpected(253
service,254
state,255
moduleIndex,256
source,257
guard,258
primitive("bool"),259
)260
}261
case None => {262
coverage = mergePatternCoverage(coverage, caseCoverage)263
}264
}265
match child(foundCase, "body") {266
case Some(body) => if body.kind == "match-case-return" {267
checkReturn(service, state, moduleIndex, source, body)268
} else {269
let terminating = expressionTerminates(body)270
let branch = if terminating {271
inferExpression(service, state, moduleIndex, source, body)272
} else {273
match currentExpectedExpression(state, node) {274
case Some(expected) => checkExpressionExpected(275
service,276
state,277
moduleIndex,278
source,279
body,280
expected,281
)282
case None => inferBranchExpression(283
service,284
state,285
moduleIndex,286
source,287
body,288
contextual,289
)290
}291
}292
if !terminating {293
branches.push(branch)294
}295
}296
case None => ()297
}298
}299
let gap = coverageGap(state, moduleIndex, coverage, scrutinee)300
if gap.decidable && gap.missing.length > 0 {301
addDiagnostic(302
state,303
moduleIndex,304
"TPZ5021",305
"non-exhaustive match: missing {stringJoin(gap.missing, ", ")}",306
"",307
node,308
)309
}310
let expectedContext = match currentExpectedExpression(state, node) {311
case Some(expected) => !hasHole(expected)312
case None => false313
}314
joinedTypes(state, branches, !(contextual || expectedContext))315
}317
function currentLoopFloor(state: CheckerState) -> int {318
if state.loopFloors.length == 0 {319
return 0320
}321
match state.loopFloors.get(state.loopFloors.length - 1) {322
case Some(found) => found323
case None => 0324
}325
}327
function statementForExpression(328
state: CheckerState,329
node: SyntaxNode,330
) -> bool {331
if state.statementExpressionContexts.length == 0 {332
return false333
}334
match state.statementExpressionContexts.get(335
state.statementExpressionContexts.length - 1,336
) {337
case Some(lo) => lo == node.lo338
case None => false339
}340
}342
function resolveLoopTarget(343
state: CheckerState,344
moduleIndex: int,345
source: Bytes,346
node: SyntaxNode,347
keyword: string,348
) -> int {349
let floor = currentLoopFloor(state)350
match child(node, "label") {351
case Some(label) => {352
let name = text(source, label)353
let mut index = state.loopFrames.length - 1354
while index >= floor {355
match state.loopFrames.get(index) {356
case Some(frame) => if frame.label == name {357
return index358
}359
case None => ()360
}361
index -= 1362
}363
addDiagnostic(364
state,365
moduleIndex,366
"TPZ5720",367
"no loop labeled `'{name}` in scope",368
"",369
node,370
)371
-1372
}373
case None => if state.loopFrames.length <= floor {374
addDiagnostic(375
state,376
moduleIndex,377
"TPZ5001",378
"`{keyword}` outside a loop",379
"",380
node,381
)382
-1383
} else {384
let index = state.loopFrames.length - 1385
match state.loopFrames.get(index) {386
case Some(frame) => if frame.bareTarget {387
index388
} else {389
addDiagnostic(390
state,391
moduleIndex,392
"TPZ5001",393
"bare `{keyword}` cannot target {frame.bareError}",394
"",395
node,396
)397
-1398
}399
case None => -1400
}401
}402
}403
}405
export function checkLoopExitStatement(406
service: CheckerService,407
state: CheckerState,408
moduleIndex: int,409
source: Bytes,410
node: SyntaxNode,411
) -> () {412
let keyword = if node.kind == "statement/break" {413
"break"414
} else {415
"continue"416
}417
let targetIndex = resolveLoopTarget(418
state,419
moduleIndex,420
source,421
node,422
keyword,423
)424
if keyword == "continue" {425
return ()426
}427
let expected = if targetIndex >= 0 {428
match state.loopFrames.get(targetIndex) {429
case Some(frame) => frame.expected430
case None => None431
}432
} else {433
None434
}435
let valueType = match child(node, "value") {436
case Some(value) => match expected {437
case Some(expectedType) => {438
let inferred = inferExpressionWithExpected(439
service,440
state,441
moduleIndex,442
source,443
value,444
expectedType,445
false,446
)447
refineExpressionToExpected(448
state,449
moduleIndex,450
value,451
inferred,452
expectedType,453
)454
}455
case None => inferExpression(service, state, moduleIndex, source, value)456
}457
case None => primitive("unit")458
}459
if targetIndex < 0 {460
return ()461
}462
match state.loopFrames.get(targetIndex) {463
case Some(frame) => if frame.valueLoop {464
let mut breaks = frame.breaks465
if frame.expected == None && breaks.length > 0 {466
match breaks.get(0) {467
case Some(first) => {468
let earlier = widen(first)469
let current = widen(valueType)470
if !hasHole(earlier) && !hasHole(current) &&471
!assignable(earlier, current) &&472
!assignable(current, earlier) {473
addDiagnostic(474
state,475
moduleIndex,476
"TPZ5721",477
"`break` values targeting this loop disagree — earlier `break` is `{typeText(earlier)}`, this is `{typeText(current)}` (all break values must have the same type)",478
"",479
node,480
)481
}482
}483
case None => ()484
}485
}486
breaks.push(valueType)487
let mut frames = state.loopFrames488
frames[targetIndex] = LoopFrame { ...frame, breaks: breaks }489
}490
case Some(_) => ()491
case None => ()492
}493
()494
}496
function inferLoopExpression(497
service: CheckerService,498
state: CheckerState,499
moduleIndex: int,500
source: Bytes,501
node: SyntaxNode,502
) -> TypeValue {503
let expected = currentExpectedExpression(state, node)504
let frame = LoopFrame {505
label: match child(node, "label") {506
case Some(label) => text(source, label)507
case None => ""508
},509
valueLoop: true,510
bareTarget: true,511
bareError: "",512
expected: expected,513
breaks: [],514
}515
let mut frames = state.loopFrames516
frames.push(frame)517
match child(node, "body") {518
case Some(body) => {519
let _ = serviceCheckBlock(520
service,521
state,522
moduleIndex,523
source,524
body,525
)526
}527
case None => ()528
}529
let completed = match frames.get(frames.length - 1) {530
case Some(found) => found531
case None => frame532
}533
frames.pop()534
if completed.breaks.length == 0 {535
primitive("unit")536
} else {537
match expected {538
case Some(_) => unionType(completed.breaks)539
case None => widenedUnion(completed.breaks)540
}541
}542
}544
function inferForExpression(545
service: CheckerService,546
state: CheckerState,547
moduleIndex: int,548
source: Bytes,549
node: SyntaxNode,550
) -> TypeValue {551
let statementPosition = statementForExpression(state, node)552
let iteratorNode = child(node, "iterator")553
let iterator = match iteratorNode {554
case Some(found) => inferExpression(service, state, moduleIndex, source, found)555
case None => emptyType("unknown")556
}557
match child(node, "pattern") {558
case Some(pattern) => patternBindingType(559
service,560
state,561
moduleIndex,562
source,563
pattern,564
checkedIterableElement(565
state,566
moduleIndex,567
match iteratorNode {568
case Some(found) => found569
case None => node570
},571
iterator,572
),573
false,574
)575
case None => ()576
}577
let mut frames = state.loopFrames578
frames.push(LoopFrame {579
label: "",580
valueLoop: false,581
bareTarget: statementPosition,582
bareError: "a value-collecting `for` (§5)",583
expected: None,584
breaks: [],585
})586
let bodyType = match child(node, "body") {587
case Some(body) => serviceCheckBlock(588
service,589
state,590
moduleIndex,591
source,592
body,593
)594
case None => primitive("unit")595
}596
frames.pop()597
if statementPosition {598
primitive("unit")599
} else {600
constructor("Array", [widen(bodyType)])601
}602
}604
function inferConcurrentExpression(605
service: CheckerService,606
state: CheckerState,607
moduleIndex: int,608
source: Bytes,609
node: SyntaxNode,610
) -> TypeValue {611
match child(node, "timeout") {612
case Some(found) => {613
if found.kind == "expression/duration" &&614
!durationFitsMilliseconds(found) {615
addDiagnostic(616
state,617
moduleIndex,618
"TPZ5001",619
"`concurrent` timeout duration must fit in u64 milliseconds (§15)",620
"",621
found,622
)623
}624
let timeoutType = inferExpression(service, state, moduleIndex, source, found)625
if timeoutType.kind == "unknown" {626
addNode(state, moduleIndex, "expression", found, timeoutType)627
}628
}629
case None => ()630
}631
let mut names: Array<string> = []632
let mut values: Array<TypeValue> = []633
for arm in children(node, "arms") {634
let nameNode = match child(arm, "name") {635
case Some(found) => found636
case None => arm637
}638
let name = text(source, nameNode)639
let mut duplicate = false640
for existing in names {641
if existing == name {642
duplicate = true643
}644
}645
if duplicate {646
addDiagnostic(647
state,648
moduleIndex,649
"TPZ5008",650
"duplicate concurrent arm `{name}`",651
"",652
nameNode,653
)654
}655
let value = match child(arm, "value") {656
case Some(found) => widen(inferExpression(657
service,658
state,659
moduleIndex,660
source,661
found,662
))663
case None => emptyType("unknown")664
}665
names.push(name)666
values.push(value)667
}668
let record = structural(names, values)669
match child(node, "else") {670
case Some(found) => {671
if hasHole(record) || blockTerminates(found) {672
let _ = serviceCheckBlock(service, state, moduleIndex, source, found)673
} else {674
let _ = serviceCheckBlockExpected(service, state, moduleIndex, source, found, record)675
}676
}677
case None => ()678
}679
if hasHole(record) {680
emptyType("unknown")681
} else {682
record683
}684
}686
function checkerByteAt(bytes: Bytes, index: int) -> int {687
match bytes.get(index) {688
case Some(value) => value689
case None => -1690
}691
}693
function durationFitsMilliseconds(node: SyntaxNode) -> bool {694
let maximum = match attribute(node, "unit") {695
case "ms" => MAX_DURATION_MILLISECONDS696
case "s" => MAX_DURATION_SECONDS697
case "m" => MAX_DURATION_MINUTES698
case _ => return false699
}700
let value = Bytes.encodeUtf8(attribute(node, "valueDecimal"))701
let limit = Bytes.encodeUtf8(maximum)702
let mut start = 0703
while start + 1 < value.length() && checkerByteAt(value, start) == 48 {704
start += 1705
}706
let digits = value.length() - start707
if digits < limit.length() {708
return true709
}710
if digits > limit.length() {711
return false712
}713
let mut index = 0714
while index < digits {715
let actual = checkerByteAt(value, start + index)716
let allowed = checkerByteAt(limit, index)717
if actual < allowed {718
return true719
}720
if actual > allowed {721
return false722
}723
index += 1724
}725
true726
}728
function constantZero(node: SyntaxNode) -> bool {729
if node.kind == "expression/integer" {730
return toInt(attribute(node, "valueDecimal")) == Some(0)731
}732
if node.kind == "expression/parenthesized" ||733
node.kind == "expression/unary" &&734
(attribute(node, "operator") == "plus" || attribute(node, "operator") == "minus") {735
return match child(node, "operand") {736
case Some(found) => constantZero(found)737
case None => false738
}739
}740
false741
}743
function inferRangeExpression(744
service: CheckerService,745
state: CheckerState,746
moduleIndex: int,747
source: Bytes,748
node: SyntaxNode,749
) -> TypeValue {750
for fieldName in ["low", "high", "step"] {751
match child(node, fieldName) {752
case Some(found) => {753
let endpoint = widen(inferExpression(754
service,755
state,756
moduleIndex,757
source,758
found,759
))760
if !hasHole(endpoint) &&761
(endpoint.kind != "primitive" || endpoint.name != "int") {762
addDiagnostic(763
state,764
moduleIndex,765
"TPZ5001",766
if fieldName == "step" {767
"a range step is `int`, found `{typeText(endpoint)}`"768
} else {769
"range endpoints are `int`, found `{typeText(endpoint)}`"770
},771
"",772
found,773
)774
}775
if fieldName == "step" && constantZero(found) {776
addDiagnostic(state, moduleIndex, "TPZ5001",777
"the range step must not be zero (§10)", "", found)778
}779
}780
case None => ()781
}782
}783
constructor("Range", [primitive("int")])784
}786
function sameBinaryType(787
left: TypeValue,788
right: TypeValue,789
kind: string,790
name: string,791
) -> bool {792
left.kind == kind && right.kind == kind &&793
left.name == name && right.name == name794
}796
export function inferBinaryExpression(797
service: CheckerService,798
state: CheckerState,799
moduleIndex: int,800
source: Bytes,801
node: SyntaxNode,802
) -> TypeValue {803
let leftNode = child(node, "left")804
let left = match leftNode {805
case Some(found) => inferExpression(service, state, moduleIndex, source, found)806
case None => emptyType("unknown")807
}808
let operator = attribute(node, "operator")809
if operator == "coalesce" {810
let optional = if left.kind == "constructor" && left.name == "Option" {811
left.arguments.get(0)812
} else if left.kind == "union" {813
let mut nullable = false814
let mut remaining: Array<TypeValue> = []815
for member in left.members {816
if member.kind == "literal" && member.name == "null" {817
nullable = true818
} else {819
remaining.push(member)820
}821
}822
if nullable && remaining.length > 0 {823
Some(unionType(remaining))824
} else {825
None826
}827
} else {828
None829
}830
return match optional {831
case Some(inner) => {832
match child(node, "right") {833
case Some(found) => {834
let _ = checkExpressionExpected(835
service,836
state,837
moduleIndex,838
source,839
found,840
inner,841
)842
}843
case None => ()844
}845
inner846
}847
case None => {848
match child(node, "right") {849
case Some(found) => { let _ = inferExpression(service, state, moduleIndex, source, found) }850
case None => ()851
}852
if !hasHole(left) {853
addDiagnostic(854
state,855
moduleIndex,856
"TPZ5001",857
"`??` needs an Option or nullable left operand, found `{typeText(left)}`",858
"",859
node,860
)861
}862
emptyType("unknown")863
}864
}865
}866
let right = match child(node, "right") {867
case Some(found) => inferExpression(service, state, moduleIndex, source, found)868
case None => emptyType("unknown")869
}870
let mut lhs = widen(left)871
let mut rhs = widen(right)872
if operator == "equal" || operator == "not-equal" {873
if hasPartialHole(lhs) && !hasHole(rhs) {874
lhs = solvePartialTypeAgainst(state, lhs, rhs)875
} else if hasPartialHole(rhs) && !hasHole(lhs) {876
rhs = solvePartialTypeAgainst(state, rhs, lhs)877
}878
}879
if hasHole(lhs) || hasHole(rhs) {880
return emptyType("unknown")881
}882
match operator {883
case "power" | "divide" => if sameBinaryType(884
lhs,885
rhs,886
"primitive",887
"int",888
) {889
primitive("int")890
} else if sameBinaryType(lhs, rhs, "primitive", "float") {891
primitive("float")892
} else {893
addDiagnostic(894
state,895
moduleIndex,896
"TPZ5001",897
"arithmetic needs matching numeric operands, found `{typeText(lhs)}` and `{typeText(rhs)}`",898
"",899
node,900
)901
emptyType("unknown")902
}903
case "multiply" | "subtract" => if sameBinaryType(904
lhs,905
rhs,906
"primitive",907
"int",908
) {909
primitive("int")910
} else if sameBinaryType(lhs, rhs, "primitive", "float") {911
primitive("float")912
} else if sameBinaryType(lhs, rhs, "big-int", "") {913
emptyType("big-int")914
} else if sameBinaryType(lhs, rhs, "decimal", "") {915
emptyType("decimal")916
} else {917
addDiagnostic(918
state,919
moduleIndex,920
"TPZ5001",921
"arithmetic needs matching numeric operands, found `{typeText(lhs)}` and `{typeText(rhs)}`",922
"",923
node,924
)925
emptyType("unknown")926
}927
case "remainder" => if sameBinaryType(lhs, rhs, "primitive", "int") {928
primitive("int")929
} else {930
addDiagnostic(931
state,932
moduleIndex,933
"TPZ5001",934
"`%` needs int operands in v5.2, found `{typeText(lhs)}` and `{typeText(rhs)}`",935
"",936
node,937
)938
emptyType("unknown")939
}940
case "add" => if sameBinaryType(lhs, rhs, "primitive", "int") {941
primitive("int")942
} else if sameBinaryType(lhs, rhs, "primitive", "float") {943
primitive("float")944
} else if sameBinaryType(lhs, rhs, "primitive", "string") {945
primitive("string")946
} else if sameBinaryType(lhs, rhs, "big-int", "") {947
emptyType("big-int")948
} else if sameBinaryType(lhs, rhs, "decimal", "") {949
emptyType("decimal")950
} else {951
addDiagnostic(952
state,953
moduleIndex,954
"TPZ5001",955
"`+` needs matching int, float, string, BigInt, or Decimal operands, found `{typeText(lhs)}` and `{typeText(rhs)}`",956
"",957
node,958
)959
emptyType("unknown")960
}961
case "less-than" | "less-or-equal" | "greater-than" |962
"greater-or-equal" => {963
let scalar = sameBinaryType(lhs, rhs, "primitive", "int") ||964
sameBinaryType(lhs, rhs, "primitive", "float") ||965
sameBinaryType(lhs, rhs, "primitive", "string") ||966
sameBinaryType(lhs, rhs, "big-int", "") ||967
sameBinaryType(lhs, rhs, "decimal", "")968
let nominal = (assignable(lhs, rhs) || assignable(rhs, lhs)) &&969
orderCapability(state, moduleIndex, lhs, []) == "accept" &&970
orderCapability(state, moduleIndex, rhs, []) == "accept"971
if scalar || nominal {972
primitive("bool")973
} else {974
addDiagnostic(975
state,976
moduleIndex,977
"TPZ5007",978
"`{typeText(lhs)}` and `{typeText(rhs)}` are not ordered comparable",979
"",980
node,981
)982
emptyType("unknown")983
}984
}985
case "equal" | "not-equal" => if !comparableType(986
state,987
moduleIndex,988
lhs,989
[],990
) || !comparableType(state, moduleIndex, rhs, []) {991
addDiagnostic(992
state,993
moduleIndex,994
"TPZ5007",995
"`{typeText(lhs)}` and `{typeText(rhs)}` are not comparable values",996
"",997
node,998
)999
emptyType("unknown")1000
} else if assignable(lhs, rhs) || assignable(rhs, lhs) {SHA256 da2ff50431d982324e69abe277c266c6312319a1ce6acfce334e3f59d430e8ee