diff --git a/README.MD b/README.MD index 017b50d..c5ebbe2 100644 --- a/README.MD +++ b/README.MD @@ -401,6 +401,13 @@ fn main() { 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 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()`, `e.count()`, and iteration in `for` - constants: `const NAME = 42` (compile-time integer/bool values) diff --git a/compiler/ast.v b/compiler/ast.v index a6880a3..1610034 100644 --- a/compiler/ast.v +++ b/compiler/ast.v @@ -100,12 +100,16 @@ pub mut: 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 mut: - name string - fields []string - line int + name string + fields []string + field_types []string + line int } pub struct FnDecl { diff --git a/compiler/check.v b/compiler/check.v index de9f3be..ffbec3c 100644 --- a/compiler/check.v +++ b/compiler/check.v @@ -44,6 +44,7 @@ mut: mutable map[string]bool // local names bound with `mut`, params, loop vars; only these may be reassigned fns map[string]FnSig structs map[string][]string + struct_types map[string]map[string]string // struct name -> field -> declared type ('' = any) enums map[string][]string consts map[string]TypeInfo loop_depth int @@ -60,6 +61,7 @@ fn check(prog Program) ! { return error('duplicate struct declaration "${sd.name}" (line ${sd.line})') } c.structs[sd.name] = sd.fields + c.struct_types[sd.name] = field_type_map(sd) } for ed in prog.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 } } - // 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 { if imp.name.len > 0 { c.modules[imp.name] = true } 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 { c.check_fn(fd)! } @@ -98,6 +106,94 @@ fn def_count(fd FnDecl) int { 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) ! { if path in c.checked { 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}"') } src := os.read_file(resolved) or { return error('cannot read import "${path}"') } 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 // functions under both the bare name and the "mod.fn" name, so internal // calls check against the former and program calls against the latter) for sd in prog.structs { if sd.name !in c.structs { c.structs[sd.name] = sd.fields + c.struct_types[sd.name] = field_type_map(sd) } } 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 { - c.check_import(imp.path, imp.name)! + // every declaration of this file (and its imports) is merged now + for sd in prog.structs { + c.validate_struct(sd)! } for fd in prog.fns { c.check_fn(fd)! @@ -193,11 +296,22 @@ fn (mut c Checker) check_stmt(st Stmt) ! { _ = c.check_expr(st.idx)! c.expect_container(base, 'index assignment', st.line)! _ = c.check_expr(st.expr)! - } - .field_assign { + } .field_assign { base := c.check_expr(st.base)! 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 { _ = 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})') } 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 } } @@ -365,6 +483,18 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo { if base.kind == .enum_t { 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 } } .method_call { diff --git a/compiler/lexer.v b/compiler/lexer.v index d272a47..22ccaf1 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -121,6 +121,10 @@ fn (mut l Lexer) next() !Tok { l.advance() return Tok{ kind: .colon, lit: ':', line: line, col: col } } + `?` { + l.advance() + return Tok{ kind: .question, lit: '?', line: line, col: col } + } `+` { l.advance() if l.peek() == `=` { diff --git a/compiler/parser.v b/compiler/parser.v index 472c14b..6c09005 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -3,7 +3,7 @@ // Grammar (informal): // program := import* (struct | enum | const | fn)* // 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)*] '}' // const := 'const' IDENT '=' expr // fn := 'fn' [ '(' IDENT IDENT ')' ] IDENT '(' [IDENT (',' IDENT)*] ')' block @@ -103,24 +103,40 @@ fn (mut p Parser) parse_program() !Program { 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 { t := p.expect(.kw_struct, "'struct'")! name := p.expect(.ident, 'struct name')! p.expect(.lbrace, "'{'")! mut fields := []string{} + mut field_types := []string{} if p.cur().kind != .rbrace { for { 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 { p.advance() continue } + if p.cur().kind != .rbrace { + // newline-separated field + continue + } break } } 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 diff --git a/compiler/tokens.v b/compiler/tokens.v index 4e0ddf2..53a70c4 100644 --- a/compiler/tokens.v +++ b/compiler/tokens.v @@ -17,6 +17,7 @@ pub enum TokKind { comma dot colon + question plus minus star diff --git a/examples/struct_types.vr b/examples/struct_types.vr new file mode 100644 index 0000000..a44dbbd --- /dev/null +++ b/examples/struct_types.vr @@ -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) +}