struct types

This commit is contained in:
allexanderbergmns
2026-08-26 12:19:23 +02:00
parent 68b3a3673c
commit 1d829c3ef2
7 changed files with 237 additions and 14 deletions
+7
View File
@@ -401,6 +401,13 @@ fn main() {
field access `a.b` and assignment `a.b = v` (chained: `a[i].b`, `a.b[i]`); field access `a.b` and assignment `a.b = v` (chained: `a[i].b`, `a.b[i]`);
structs are mutable references (identity `==`/`!=`), and setting a missing structs are mutable references (identity `==`/`!=`), and setting a missing
field adds it, so records can be built incrementally field adds it, so records can be built incrementally
- declared struct field types:
`struct Point { x int, y int, tag ?string, next Point }` — fields may declare
a type (`int`, `float`, `string`, `bool`, `array`, another struct, or an
enum); a leading `?` also accepts `none`. The checker validates struct
literals and field reads/writes against the declaration: unknown fields and
mistyped values are compile-time errors. Untyped fields stay fully dynamic,
and ints widen to `float` fields
- enums: `enum Color { red green blue }` with `Color.red`, `e.to_string()`, - enums: `enum Color { red green blue }` with `Color.red`, `e.to_string()`,
`e.count()`, and iteration in `for` `e.count()`, and iteration in `for`
- constants: `const NAME = 42` (compile-time integer/bool values) - constants: `const NAME = 42` (compile-time integer/bool values)
+8 -4
View File
@@ -100,12 +100,16 @@ pub mut:
line int line int
} }
// StructDecl is a `struct Name { a, b }` declaration. // StructDecl is a `struct Name { a int, b }` declaration.
// field_types is parallel to fields; an empty string means the field is
// dynamically typed (any value), while a declared type is enforced by the
// checker. A leading `?` marks the field as also accepting `none`.
pub struct StructDecl { pub struct StructDecl {
pub mut: pub mut:
name string name string
fields []string fields []string
line int field_types []string
line int
} }
pub struct FnDecl { pub struct FnDecl {
+137 -7
View File
@@ -44,6 +44,7 @@ mut:
mutable map[string]bool // local names bound with `mut`, params, loop vars; only these may be reassigned mutable map[string]bool // local names bound with `mut`, params, loop vars; only these may be reassigned
fns map[string]FnSig fns map[string]FnSig
structs map[string][]string structs map[string][]string
struct_types map[string]map[string]string // struct name -> field -> declared type ('' = any)
enums map[string][]string enums map[string][]string
consts map[string]TypeInfo consts map[string]TypeInfo
loop_depth int loop_depth int
@@ -60,6 +61,7 @@ fn check(prog Program) ! {
return error('duplicate struct declaration "${sd.name}" (line ${sd.line})') return error('duplicate struct declaration "${sd.name}" (line ${sd.line})')
} }
c.structs[sd.name] = sd.fields c.structs[sd.name] = sd.fields
c.struct_types[sd.name] = field_type_map(sd)
} }
for ed in prog.enums { for ed in prog.enums {
if ed.name in c.enums { if ed.name in c.enums {
@@ -76,13 +78,19 @@ fn check(prog Program) ! {
} }
c.fns[fd.name] = FnSig{ min_args: fd.params.len - def_count(fd), has_defs: fd.has_defs, variadic: fd.variadic, n_type_params: fd.type_params.len } c.fns[fd.name] = FnSig{ min_args: fd.params.len - def_count(fd), has_defs: fd.has_defs, variadic: fd.variadic, n_type_params: fd.type_params.len }
} }
// imported files are checked (and their symbols merged) recursively // imported files are checked (and their symbols merged) recursively;
// recursion runs first so a struct's field types may reference types
// declared in transitive imports
for imp in prog.imports { for imp in prog.imports {
if imp.name.len > 0 { if imp.name.len > 0 {
c.modules[imp.name] = true c.modules[imp.name] = true
} }
c.check_import(imp.path, imp.name)! c.check_import(imp.path, imp.name)!
} }
// all declarations are visible now — validate declared field types
for sd in prog.structs {
c.validate_struct(sd)!
}
for fd in prog.fns { for fd in prog.fns {
c.check_fn(fd)! c.check_fn(fd)!
} }
@@ -98,6 +106,94 @@ fn def_count(fd FnDecl) int {
return n return n
} }
// field_type_map turns a StructDecl's parallel fields/field_types arrays into
// a lookup map (missing entries default to '' = dynamically typed).
fn field_type_map(sd StructDecl) map[string]string {
mut m := map[string]string{}
for i, fname in sd.fields {
m[fname] = if i < sd.field_types.len { sd.field_types[i] } else { '' }
}
return m
}
const builtin_type_names = ['int', 'float', 'string', 'bool', 'array']
// validate_struct checks a struct declaration's own well-formedness: no
// duplicate fields and every declared type names a builtin or a known
// struct/enum.
fn (mut c Checker) validate_struct(sd StructDecl) ! {
mut seen := map[string]bool{}
for i, fname in sd.fields {
if fname in seen {
return error('duplicate field "${fname}" in struct ${sd.name} (line ${sd.line})')
}
seen[fname] = true
decl := if i < sd.field_types.len { sd.field_types[i] } else { '' }
if decl.len == 0 {
continue
}
base := decl.trim_left('?')
if base in builtin_type_names {
continue
}
if base in c.structs || base in c.enums {
continue
}
return error('unknown type "${base}" for field "${fname}" of struct ${sd.name} (line ${sd.line})')
}
}
// field_accepts reports whether a value of checker type t may be stored in a
// field declared as decl ('' = any). Ints widen to floats; unknown stays
// dynamic. A '?'-prefixed declaration additionally accepts none.
fn (mut c Checker) field_accepts(decl string, t TypeInfo) bool {
if t.kind == .unknown || decl.len == 0 {
return true
}
if t.kind == .none_t {
return decl.starts_with('?')
}
base := decl.trim_left('?')
return match base {
'int' { t.kind == .int_t }
'float' { t.kind == .float_t || t.kind == .int_t }
'string' { t.kind == .string_t }
'bool' { t.kind == .bool_t }
'array' { t.kind == .array_t }
else {
if base in c.structs {
t.kind == .struct_t && t.name == base
} else if base in c.enums {
t.kind == .enum_t && t.name == base
} else {
true // unresolved name (separate compilation) — stay conservative
}
}
}
}
// declared_type maps a declared field type to the checker TypeInfo that reads
// of that field produce.
fn (mut c Checker) declared_type(decl string) TypeInfo {
base := decl.trim_left('?')
return match base {
'int' { TypeInfo{ kind: .int_t } }
'float' { TypeInfo{ kind: .float_t } }
'string' { TypeInfo{ kind: .string_t } }
'bool' { TypeInfo{ kind: .bool_t } }
'array' { TypeInfo{ kind: .array_t } }
else {
if base in c.enums {
TypeInfo{ kind: .enum_t, name: base }
} else if base in c.structs {
TypeInfo{ kind: .struct_t, name: base }
} else {
TypeInfo{ kind: .unknown }
}
}
}
}
fn (mut c Checker) check_import(path string, mod_name string) ! { fn (mut c Checker) check_import(path string, mod_name string) ! {
if path in c.checked { if path in c.checked {
return return
@@ -106,12 +202,18 @@ fn (mut c Checker) check_import(path string, mod_name string) ! {
resolved := resolve_import(path) or { return error('cannot read import "${path}"') } resolved := resolve_import(path) or { return error('cannot read import "${path}"') }
src := os.read_file(resolved) or { return error('cannot read import "${path}"') } src := os.read_file(resolved) or { return error('cannot read import "${path}"') }
prog := parse(tokenize(src)!)! prog := parse(tokenize(src)!)!
// recurse into the file's own imports before merging, so every type it
// references is registered by the time field declarations are validated
for imp in prog.imports {
c.check_import(imp.path, imp.name)!
}
// merge declarations from the import (bare modules register their // merge declarations from the import (bare modules register their
// functions under both the bare name and the "mod.fn" name, so internal // functions under both the bare name and the "mod.fn" name, so internal
// calls check against the former and program calls against the latter) // calls check against the former and program calls against the latter)
for sd in prog.structs { for sd in prog.structs {
if sd.name !in c.structs { if sd.name !in c.structs {
c.structs[sd.name] = sd.fields c.structs[sd.name] = sd.fields
c.struct_types[sd.name] = field_type_map(sd)
} }
} }
for ed in prog.enums { for ed in prog.enums {
@@ -135,8 +237,9 @@ fn (mut c Checker) check_import(path string, mod_name string) ! {
} }
} }
} }
for imp in prog.imports { // every declaration of this file (and its imports) is merged now
c.check_import(imp.path, imp.name)! for sd in prog.structs {
c.validate_struct(sd)!
} }
for fd in prog.fns { for fd in prog.fns {
c.check_fn(fd)! c.check_fn(fd)!
@@ -193,11 +296,22 @@ fn (mut c Checker) check_stmt(st Stmt) ! {
_ = c.check_expr(st.idx)! _ = c.check_expr(st.idx)!
c.expect_container(base, 'index assignment', st.line)! c.expect_container(base, 'index assignment', st.line)!
_ = c.check_expr(st.expr)! _ = c.check_expr(st.expr)!
} } .field_assign {
.field_assign {
base := c.check_expr(st.base)! base := c.check_expr(st.base)!
c.expect_struct_like(base, 'field assignment', st.line)! c.expect_struct_like(base, 'field assignment', st.line)!
_ = c.check_expr(st.expr)! vt := c.check_expr(st.expr)!
// a known declared struct validates both field existence and type;
// anonymous structs stay dynamic (assigning adds fields)
if base.kind == .struct_t && base.name.len > 0 && base.name in c.structs {
fields := c.structs[base.name]
if st.target !in fields {
return error('unknown field "${st.target}" for struct ${base.name} (line ${st.line})')
}
decl := c.struct_types[base.name][st.target]
if !c.field_accepts(decl, vt) {
return error('cannot assign a ${type_name(vt.kind)} to field "${st.target}" (${decl}) of struct ${base.name} (line ${st.line})')
}
}
} }
.if_stmt { .if_stmt {
_ = c.check_expr(st.cond)! _ = c.check_expr(st.cond)!
@@ -338,7 +452,11 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo {
return error('duplicate field "${f.name}" in struct literal (line ${e.line})') return error('duplicate field "${f.name}" in struct literal (line ${e.line})')
} }
seen[f.name] = true seen[f.name] = true
_ = c.check_expr(f.val)! vt := c.check_expr(f.val)!
decl := c.struct_types[e.name][f.name]
if !c.field_accepts(decl, vt) {
return error('cannot assign a ${type_name(vt.kind)} to field "${f.name}" (${decl}) of struct ${e.name} (line ${e.line})')
}
} }
return TypeInfo{ kind: .struct_t, name: e.name } return TypeInfo{ kind: .struct_t, name: e.name }
} }
@@ -365,6 +483,18 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo {
if base.kind == .enum_t { if base.kind == .enum_t {
return TypeInfo{ kind: .enum_t, name: base.name } return TypeInfo{ kind: .enum_t, name: base.name }
} }
// a known declared struct validates the field name and yields its
// declared type, so chained reads keep their types
if base.kind == .struct_t && base.name.len > 0 && base.name in c.structs {
fields := c.structs[base.name]
if e.name !in fields {
return error('unknown field "${e.name}" for struct ${base.name} (line ${e.line})')
}
decl := c.struct_types[base.name][e.name]
if decl.len > 0 {
return c.declared_type(decl)
}
}
TypeInfo{ kind: .unknown } TypeInfo{ kind: .unknown }
} }
.method_call { .method_call {
+4
View File
@@ -121,6 +121,10 @@ fn (mut l Lexer) next() !Tok {
l.advance() l.advance()
return Tok{ kind: .colon, lit: ':', line: line, col: col } return Tok{ kind: .colon, lit: ':', line: line, col: col }
} }
`?` {
l.advance()
return Tok{ kind: .question, lit: '?', line: line, col: col }
}
`+` { `+` {
l.advance() l.advance()
if l.peek() == `=` { if l.peek() == `=` {
+19 -3
View File
@@ -3,7 +3,7 @@
// Grammar (informal): // Grammar (informal):
// program := import* (struct | enum | const | fn)* // program := import* (struct | enum | const | fn)*
// import := 'import' STRING | 'import' IDENT // import := 'import' STRING | 'import' IDENT
// struct := 'struct' IDENT '{' [IDENT (',' IDENT)*] '}' // struct := 'struct' IDENT '{' [field (',' field)*] '}' (field := IDENT [Type] | IDENT '?' Type)
// enum := 'enum' IDENT '{' [IDENT (',' IDENT)*] '}' // enum := 'enum' IDENT '{' [IDENT (',' IDENT)*] '}'
// const := 'const' IDENT '=' expr // const := 'const' IDENT '=' expr
// fn := 'fn' [ '(' IDENT IDENT ')' ] IDENT '(' [IDENT (',' IDENT)*] ')' block // fn := 'fn' [ '(' IDENT IDENT ')' ] IDENT '(' [IDENT (',' IDENT)*] ')' block
@@ -103,24 +103,40 @@ fn (mut p Parser) parse_program() !Program {
return prog return prog
} }
// parse_struct_decl parses `struct Name { a, b, c }`. // parse_struct_decl parses `struct Name { x int, y int, tag ?string, w }`.
// Each field is `name` (dynamically typed) or `name Type`; a leading `?` on
// the type marks it optional (also accepting `none`). Fields may be separated
// by commas or newlines.
fn (mut p Parser) parse_struct_decl() !StructDecl { fn (mut p Parser) parse_struct_decl() !StructDecl {
t := p.expect(.kw_struct, "'struct'")! t := p.expect(.kw_struct, "'struct'")!
name := p.expect(.ident, 'struct name')! name := p.expect(.ident, 'struct name')!
p.expect(.lbrace, "'{'")! p.expect(.lbrace, "'{'")!
mut fields := []string{} mut fields := []string{}
mut field_types := []string{}
if p.cur().kind != .rbrace { if p.cur().kind != .rbrace {
for { for {
fields << p.expect(.ident, 'field name')!.lit fields << p.expect(.ident, 'field name')!.lit
if p.cur().kind == .question {
p.advance()
field_types << '?' + p.expect(.ident, 'type name')!.lit
} else if p.cur().kind == .ident {
field_types << p.advance().lit
} else {
field_types << ''
}
if p.cur().kind == .comma { if p.cur().kind == .comma {
p.advance() p.advance()
continue continue
} }
if p.cur().kind != .rbrace {
// newline-separated field
continue
}
break break
} }
} }
p.expect(.rbrace, "'}'")! p.expect(.rbrace, "'}'")!
return StructDecl{ name: name.lit, fields: fields, line: t.line } return StructDecl{ name: name.lit, fields: fields, field_types: field_types, line: t.line }
} }
// parse_import parses `import "path/to/file.vr"` (flat file merge) or a // parse_import parses `import "path/to/file.vr"` (flat file merge) or a
+1
View File
@@ -17,6 +17,7 @@ pub enum TokKind {
comma comma
dot dot
colon colon
question
plus plus
minus minus
star star
+61
View File
@@ -0,0 +1,61 @@
// struct_types.vr — declared struct field types, validated by the checker.
//
// vr run examples/struct_types.vr
// vr test examples/struct_types.vr
struct Point {
x int
y int
}
struct Named {
name string
tag ?string
origin Point
}
enum Color { red green blue }
struct Painted {
c Color
}
struct Wrapper {
pt Point
items array
ratio float
ok bool
}
fn test_typed_struct_basics() {
let p = Point{ x: 3, y: 4 }
assert p.x == 3
assert p.y == 4
mut q = Point{ x: 1, y: 2 }
q.x = 10
assert q.x + q.y == 12
}
fn test_optional_and_nested() {
let a = Named{ name: "amy", tag: "admin", origin: Point{ x: 1, y: 2 } }
assert a.name == "amy"
assert a.tag == "admin"
assert a.origin.x == 1
let b = Named{ name: "bo", tag: none, origin: Point{ x: 0, y: 0 } }
assert b.tag == none
// ints widen to float fields
let w = Wrapper{ pt: Point{ x: 5, y: 6 }, items: [1, 2], ratio: 2, ok: true }
assert w.ratio == 2.0 and w.pt.y == 6
}
fn test_enum_field() {
let p = Painted{ c: Color.green }
assert p.c == Color.green
}
fn main() {
let p = Point{ x: 3, y: 4 }
println(p.x + p.y)
let n = Named{ name: "vuurraaf", tag: none, origin: p }
println(n.name + " y=" + n.origin.y)
}