mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 15:17:19 +00:00
struct types
This commit is contained in:
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub enum TokKind {
|
|||||||
comma
|
comma
|
||||||
dot
|
dot
|
||||||
colon
|
colon
|
||||||
|
question
|
||||||
plus
|
plus
|
||||||
minus
|
minus
|
||||||
star
|
star
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user