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/emitter/type_test.tpz
Topaz · 12316 B · Original file
1
import emitter.ir { Table, typeDeclaration }2
import types { PreviewTypeAtom }3
import emitter.repr { reprAt, isCopyRepr }4
import emitter.rust_text { rustStr }6
export function childTypes(atoms: Array<PreviewTypeAtom>, parent: int) -> Array<int> {7
let mut out: Array<int> = []8
let mut index = 09
for atom in atoms {10
if atom.parent == parent { out.push(index) }11
index += 112
}13
out14
}16
// Semantic atoms preserve alias expansion, structural fields and nominal17
// identities. Tests inspect the value; a container's outer tag is insufficient.18
function typeTestInner(table: Table, scopeId: string, atoms: Array<PreviewTypeAtom>, index: int, access: string, repr: string, active: Map<string, string>) -> Result<string, string> {19
if index >= atoms.length { return Err("unsupported: missing pattern type") }20
let atom = atoms[index]21
let subs = childTypes(atoms, index)22
if atom.kind == "union" {23
let mut tests: Array<string> = []24
for sub in subs { tests.push("({typeTestInner(table, scopeId, atoms, sub, access, repr, active)?})") }25
return Ok(if tests.length == 0 { "false" } else { tests.join(" || ") })26
}27
if atom.kind == "unknown" || atom.kind == "type-parameter" { return Ok("true") }28
if atom.kind == "rigid" {29
match runtimeBoundType(table, scopeId, atom.name)? {30
case Some(body) => return typeTestInner(table, scopeId, body, 0, access, repr, active)31
case None => return Ok("true")32
}33
}34
if isCopyRepr(repr) {35
if reprAt(atoms, index) != repr { return Ok("false") }36
if atom.kind != "literal" { return Ok("true") }37
return Ok("({access}) == {atom.value}")38
}39
let value = "&({access})"40
if atom.kind == "primitive" || atom.kind == "literal" {41
let variant = match atom.name {42
case "int" => "Int"43
case "float" => "Float"44
case "bool" => "Bool"45
case "string" => "Str"46
case "unit" => return Ok("matches!({value}, Value::Unit)")47
case "null" => return Ok("matches!({value}, Value::Null)")48
case "never" => return Ok("false")49
case _ => return Err("unsupported: pattern type {atom.name}")50
}51
if atom.kind == "literal" {52
let test = if atom.name == "string" { "__v.as_ref() == {rustStr(atom.value)}" } else { "*__v == {atom.value}" }53
return Ok("matches!({value}, Value::{variant}(__v) if {test})")54
}55
return Ok("matches!({value}, Value::{variant}(_))")56
}57
if atom.kind == "function" {58
let mut count = 059
let mut variadic = false60
for sub in subs {61
if atoms[sub].field == "parameters" { count += 1 }62
if atoms[sub].field == "variadic" { variadic = true }63
}64
return Ok("callable_shape_matches({value}, {count}, {variadic})")65
}66
if atom.kind == "record" {67
let mut checks: Array<string> = []68
for sub in subs {69
let test = typeTestInner(table, scopeId, atoms, sub, "(*__v)", "value", active)?70
checks.push("(match __rec.get({rustStr(atoms[sub].edgeName)}) \{ Some(__v) => {test}, None => false \})")71
}72
let rest = if checks.length == 0 { "" } else { " && {checks.join(" && ")}" }73
return Ok("match {value} \{ Value::Record(__rec) => __rec.len() == {subs.length}{rest}, _ => false \}")74
}75
if atom.kind == "constructor" {76
if atom.name == "Option" && subs.length == 1 {77
let test = typeTestInner(table, scopeId, atoms, subs[0], "(**__v)", "value", active)?78
return Ok("match {value} \{ Value::None => true, Value::Some(__v) => {test}, _ => false \}")79
}80
if atom.name == "Result" && subs.length == 2 {81
let yes = typeTestInner(table, scopeId, atoms, subs[0], "(**__v)", "value", active)?82
let no = typeTestInner(table, scopeId, atoms, subs[1], "(**__v)", "value", active)?83
return Ok("match {value} \{ Value::Ok(__v) => {yes}, Value::Err(__v) => {no}, _ => false \}")84
}85
if (atom.name == "Array" || atom.name == "Set") && subs.length == 1 {86
let test = typeTestInner(table, scopeId, atoms, subs[0], "(*__v)", "value", active)?87
let items = if atom.name == "Set" { ".items()" } else { "" }88
return Ok("match {value} \{ Value::{atom.name}(__xs) => __xs.borrow(){items}.iter().all(|__v| {test}), _ => false \}")89
}90
if atom.name == "Map" && subs.length == 2 {91
let key = typeTestInner(table, scopeId, atoms, subs[0], "__key", "value", active)?92
let val = typeTestInner(table, scopeId, atoms, subs[1], "__val", "value", active)?93
return Ok("match {value} \{ Value::Map(__xs) => __xs.borrow().pairs().into_iter().all(|(__key, __val)| ({key}) && ({val})), _ => false \}")94
}95
}96
if atom.kind == "enum" || atom.kind == "nominal-record" || atom.kind == "newtype" {97
return nominalTest(table, scopeId, atoms, index, access, active)98
}99
let variant = match atom.kind {100
case "json-value" => "Json"101
case "toml-value" => "Toml"102
case "url" => "Url"103
case "match" => "RegexMatch"104
case "bytes" => "Bytes"105
case "byte-buffer" => "ByteBuffer"106
case "path" => "Path"107
case "regex" => "Regex"108
case "date" => "Date"109
case "big-int" => "BigInt"110
case "decimal" => "Decimal"111
case "rounding-mode" => return Ok("matches!({value}, Value::Enum \{ enum_id, .. \} if enum_id.as_ref() == \"RoundingMode\")")112
case _ => ""113
}114
if variant != "" { return Ok("matches!({value}, Value::{variant}(_))") }115
return Err("unsupported: pattern type {atom.kind}/{atom.name}/{atom.identity}")116
}118
export function nominalIdentity(atom: PreviewTypeAtom) -> string {119
if atom.identity.contains("::") || atom.origin == "" { atom.identity }120
else { "{atom.origin}::{atom.identity}" }121
}123
export function subtree(atoms: Array<PreviewTypeAtom>, root: int) -> Array<PreviewTypeAtom> {124
let mut out: Array<PreviewTypeAtom> = []125
let mut indexes: Map<int, int> = Map.new()126
let mut index = root127
while index < atoms.length {128
let atom = atoms[index]129
if index > root && atom.parent < root { break }130
let parent = if index == root { -1 } else { indexes.get(atom.parent) ?? -1 }131
indexes.insert(index, out.length)132
out.push(PreviewTypeAtom { ...atom, parent: parent,133
field: if index == root { "root" } else { atom.field },134
index: if index == root { 0 } else { atom.index },135
edgeName: if index == root { "" } else { atom.edgeName } })136
index += 1137
}138
out139
}141
export function substitute(atoms: Array<PreviewTypeAtom>, params: Map<string, Array<PreviewTypeAtom>>) -> Array<PreviewTypeAtom> {142
let mut out: Array<PreviewTypeAtom> = []143
let mut indexes: Map<int, int> = Map.new()144
let mut index = 0145
for atom in atoms {146
let parent = if atom.parent < 0 { -1 } else { indexes.get(atom.parent) ?? -1 }147
indexes.insert(index, out.length)148
let actual = if atom.kind == "type-parameter" || atom.kind == "rigid" { params.get(atom.name) } else { None }149
match actual {150
case Some(types) => {151
let offset = out.length152
for t in types {153
out.push(PreviewTypeAtom { ...t,154
parent: if t.parent < 0 { parent } else { t.parent + offset },155
field: if t.parent < 0 { atom.field } else { t.field },156
index: if t.parent < 0 { atom.index } else { t.index },157
edgeName: if t.parent < 0 { atom.edgeName } else { t.edgeName } })158
}159
}160
case None => out.push(PreviewTypeAtom { ...atom, parent: parent })161
}162
index += 1163
}164
out165
}167
function nominalTest(table: Table, scopeId: string, atoms: Array<PreviewTypeAtom>, index: int, access: string,168
active: Map<string, string>) -> Result<string, string> {169
let atom = atoms[index]170
let identity = nominalIdentity(atom)171
let declaration = match table.nominals.get(identity) {172
case Some(found) => found173
case None => return Err("unsupported: nominal type declaration {identity}")174
}175
let types = subtree(atoms, index)176
let key = JSON.stringify(types)?177
match active.get(key) {178
case Some(helper) => return Ok("{helper}(&({access}))")179
case None => ()180
}181
let helper = "__nominal_type_{active.length}"182
let mut next: Map<string, string> = Map.new()183
for pair in active.entries { next.insert(pair.key, pair.value) }184
next.insert(key, helper)185
let mut params: Map<string, Array<PreviewTypeAtom>> = Map.new()186
let args = childTypes(atoms, index)187
let mut count = 0188
for parameter in declaration {189
if parameter.parent == 0 && parameter.field == "typeParameters" {190
if count >= args.length { return Err("unsupported: nominal type argument arity {identity}") }191
params.insert(parameter.name, subtree(atoms, args[count]))192
count += 1193
}194
}195
if count != args.length { return Err("unsupported: nominal type argument arity {identity}") }196
if count == 0 {197
let id = rustStr(identity)198
if atom.kind == "nominal-record" { return Ok("({access}).is_nominal_record_declaration({id})") }199
if atom.kind == "newtype" { return Ok("({access}).is_newtype_declaration({id})") }200
return Ok("matches!(&({access}), Value::Enum \{ enum_id, declaration_identity, .. \} if nominal_declaration_identity(enum_id.as_ref(), declaration_identity.as_deref()) == {id})")201
}202
let definition = substitute(declaration, params)203
let roots = childTypes(definition, 0)204
let mut checks: Array<string> = []205
let id = rustStr(identity)206
let mut body = "false"207
if atom.kind == "nominal-record" {208
for field in roots {209
if definition[field].field != "fields" { continue }210
let test = typeTestInner(table, scopeId, definition, field, "(*__field)", "value", next)?211
checks.push("__fields.iter().find(|(n, _)| n.as_ref() == {rustStr(definition[field].edgeName)}).map(|(_, __field)| {test}).unwrap_or(false)")212
}213
let fields = if checks.length == 0 { "true" } else { checks.join(" && ") }214
body = "match __value \{ Value::NominalRecord \{ record_id, declaration_identity, fields: __fields, .. \} if nominal_declaration_identity(record_id.as_ref(), declaration_identity.as_deref()) == {id} => {fields}, _ => false \}"215
} else if atom.kind == "newtype" {216
for field in roots {217
if definition[field].field != "base" { continue }218
let test = typeTestInner(table, scopeId, definition, field, "(**inner)", "value", next)?219
body = "match __value \{ Value::Newtype \{ newtype_id, declaration_identity, inner, .. \} if nominal_declaration_identity(newtype_id.as_ref(), declaration_identity.as_deref()) == {id} => {test}, _ => false \}"220
}221
()222
} else {223
for variant in roots {224
if definition[variant].field != "variants" { continue }225
let payload = childTypes(definition, variant)226
let mut tests = ["payloads.len() == {payload.length}"]227
let mut position = 0228
for field in payload {229
let test = typeTestInner(table, scopeId, definition, field, "payloads[{position}]", "value", next)?230
tests.push("({test})")231
position += 1232
}233
checks.push("{rustStr(definition[variant].name)} if {tests.join(" && ")} => true")234
}235
body = "match __value \{ Value::Enum \{ enum_id, declaration_identity, variant, payloads, .. \} if nominal_declaration_identity(enum_id.as_ref(), declaration_identity.as_deref()) == {id} => match variant.as_ref() \{ {checks.join(", ")}, _ => false \}, _ => false \}"236
}237
Ok("\{ fn {helper}(__value: &Value) -> bool \{ {body} \} {helper}(&({access})) \}")238
}240
export function typeTest(table: Table, scopeId: string, atoms: Array<PreviewTypeAtom>, index: int, access: string, repr: string) -> Result<string, string> {241
let active: Map<string, string> = Map.new()242
typeTestInner(table, scopeId, atoms, index, access, repr, active)243
}245
// Named runtime types take precedence over erasing a generic parameter.246
function runtimeBoundType(table: Table, scopeId: string, name: string) -> Result<Option<Array<PreviewTypeAtom>>, string> {247
let declaration = match typeDeclaration(table, scopeId, name)? {248
case Some(found) => found249
case None => return Ok(None)250
}251
if declaration.kind != "type-alias" { return Ok(Some([declaration.typeDefinition[0]])) }252
let mut index = 0253
for atom in declaration.typeDefinition {254
if atom.parent == 0 && atom.field == "typeParameters" {255
return Err("unsupported: generic alias arity {name}")256
}257
if atom.parent == 0 && atom.field == "base" {258
return Ok(Some(subtree(declaration.typeDefinition, index)))259
}260
index += 1261
}262
Err("unsupported: alias definition {name}")263
}SHA256 2b42cb0150fa44bec47b987e90f7eb581710b6ee60c92daa4698c86609b432ac