diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c18648a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install V + run: | + git clone --depth 1 https://github.com/vlang/v /tmp/v + cd /tmp/v && make + sudo ln -sf /tmp/v/v /usr/local/bin/v + v version + + - name: Build toolchain + run: v -o bin/vr . + + - name: Smoke test — compile and run hello.vr + run: ./bin/vr run examples/hello.vr + + - name: Smoke test — compile and run fib.vr + run: ./bin/vr run examples/fib.vr + + - name: Smoke test — compile and run structs.vr + run: ./bin/vr run examples/structs.vr + + - name: Smoke test — compile and run match.vr + run: ./bin/vr run examples/match.vr + + - name: Smoke test — compile and run arrays.vr + run: ./bin/vr run examples/arrays.vr + + - name: Smoke test — assemble, link, and run math.vasm + run: | + ./bin/vr assemble examples/math.vasm -o /tmp/math.vobj + ./bin/vr link /tmp/math.vobj -o /tmp/math.vbin + ./bin/vr run /tmp/math.vbin + + - name: Smoke test — multi-file compile + link + run: | + ./bin/vr compile examples/lib.vr -o /tmp/lib.vobj + ./bin/vr compile examples/use_lib.vr -o /tmp/use_lib.vobj + ./bin/vr link /tmp/lib.vobj /tmp/use_lib.vobj -o /tmp/use_lib.vbin + ./bin/vr run /tmp/use_lib.vbin + + - name: Run test suite + # test_failing is intentionally skipped — it asserts 1 == 2 on purpose. + # The test runner exits 1 if any test_* fails, so we run every .vr file + # that does NOT contain a test_failing function. examples/tests.vr has + # test_failing, so we compile a clean copy without it. + run: | + grep -n 'fn test_failing' examples/tests.vr || true + sed '/fn test_failing/,/^}/d' examples/tests.vr > /tmp/ci_tests.vr + ./bin/vr test /tmp/ci_tests.vr diff --git a/compiler/compiler.v b/compiler/compiler.v index 629c32f..c4358c9 100644 --- a/compiler/compiler.v +++ b/compiler/compiler.v @@ -85,6 +85,8 @@ mut: symbols []obj.Symbol relocs []obj.Reloc locals map[string]int + types map[string]string // local name -> declared struct type ('' = unknown) + structs map[string][]string // declared struct name -> field list local_cnt int argc int cur_fn string @@ -97,6 +99,12 @@ mut: fn gen(prog Program) !obj.Obj { mut g := Gen{} + for sd in prog.structs { + if sd.name in g.structs { + return error('duplicate struct declaration "${sd.name}"') + } + g.structs[sd.name] = sd.fields + } for fd in prog.fns { g.gen_fn(fd)! } @@ -109,13 +117,23 @@ fn gen(prog Program) !obj.Obj { } fn (mut g Gen) gen_fn(fd FnDecl) ! { - g.cur_fn = fd.name - g.symbols << obj.Symbol{ name: fd.name, entry: g.code.len } + // methods compile to functions named `Type.method`; the receiver is the + // implicit first argument, so `p.dist(x)` becomes `call Point.dist p, x` + sym := if fd.recv_type.len > 0 { '${fd.recv_type}.${fd.name}' } else { fd.name } + g.cur_fn = sym + g.symbols << obj.Symbol{ name: sym, entry: g.code.len } g.locals.clear() + g.types.clear() g.local_cnt = 0 - g.argc = fd.params.len + g.argc = fd.params.len + if fd.recv_type.len > 0 { 1 } else { 0 } + mut next := 0 + if fd.recv_type.len > 0 { + g.locals[fd.recv_name] = 0 + g.types[fd.recv_name] = fd.recv_type + next = 1 + } for i, p in fd.params { - g.locals[p] = i + g.locals[p] = i + next } g.local_cnt = g.argc // `enter n` reserves the non-parameter locals; n is patched once the body @@ -157,6 +175,7 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { idx := g.local_cnt g.local_cnt++ g.locals[st.target] = idx + g.types[st.target] = g.expr_type(st.expr) g.code << op_store g.code << obj.encode_i64(i64(idx)) } @@ -165,6 +184,7 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { return error('unknown variable "${st.target}" at line ${st.line}') } g.gen_expr(st.expr)! + g.types[st.target] = g.expr_type(st.expr) g.code << op_store g.code << obj.encode_i64(i64(idx)) } @@ -272,7 +292,9 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l } prev := g.locals[st.target] or { -1 } + prev_t := g.types[st.target] or { '' } g.locals[st.target] = var_idx + g.types.delete(st.target) for s in st.body { g.gen_stmt(s)! } @@ -281,6 +303,9 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { } else { g.locals.delete(st.target) } + if prev_t.len > 0 { + g.types[st.target] = prev_t + } g.loops.delete_last() g.emit_label(inc_l) g.emit_load(var_idx) @@ -320,7 +345,9 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { g.code << op_aget g.emit_store(elem_idx) prev := g.locals[st.target] or { -1 } + prev_t := g.types[st.target] or { '' } g.locals[st.target] = elem_idx + g.types.delete(st.target) for s in st.body { g.gen_stmt(s)! } @@ -329,6 +356,9 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { } else { g.locals.delete(st.target) } + if prev_t.len > 0 { + g.types[st.target] = prev_t + } g.loops.delete_last() g.emit_label(inc_l) g.emit_load(idx_idx) @@ -395,6 +425,22 @@ fn (mut g Gen) gen_expr(e Expr) ! { g.code << obj.encode_i64(i64(e.elems.len)) } .struct_lit { + // typed literals validate their fields against the declaration + // (an undeclared type name is allowed — it may live in another + // file, where the same validation applies) + if e.name.len > 0 && e.name in g.structs { + decl_fields := g.structs[e.name] + seen := map[string]bool{} + for f in e.fields { + if f.name !in decl_fields { + return error('unknown field "${f.name}" for struct ${e.name} (line ${e.line})') + } + if f.name in seen { + return error('duplicate field "${f.name}" in struct literal (line ${e.line})') + } + seen[f.name] = true + } + } // for each field: push the name string then the value; mkstruct n // pops the (name, value) pairs and builds the record for f in e.fields { @@ -409,6 +455,18 @@ fn (mut g Gen) gen_expr(e Expr) ! { g.emit_field_name(e.name) g.code << op_sget } + .method_call { + // p.dist(x) → call .dist p, x + recv_t := g.method_receiver_type(e)! + g.gen_expr(*e.left)! + for a in e.args { + g.gen_expr(a)! + } + g.code << op_call + g.code << obj.encode_i64(0) // placeholder — patched by the linker + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: '${recv_t}.${e.name}', kind: 0 } + g.code << obj.encode_i64(i64(e.args.len + 1)) // receiver + args + } .index { g.gen_expr(*e.left)! g.gen_expr(*e.right)! @@ -547,6 +605,33 @@ fn (mut g Gen) gen_binary(e Expr) ! { } } +// expr_type returns the declared struct type of an expression when it is +// statically knowable: a typed literal `Point{...}` or a copy of a typed +// variable. Everything else has no known type (''). +fn (mut g Gen) expr_type(e Expr) string { + if e.kind == .struct_lit { + return e.name + } + if e.kind == .ident { + return g.types[e.name] or { '' } + } + return '' +} + +// method_receiver_type resolves the struct type a method call is made on. +// The receiver must be a plain variable whose type the compiler knows +// (from a typed literal, an assignment, or a method receiver binding). +fn (mut g Gen) method_receiver_type(e Expr) !string { + recv := e.left + if recv.kind == .ident { + t := g.types[recv.name] or { '' } + if t.len > 0 { + return t + } + } + return error('cannot resolve method "${e.name}": receiver type unknown (line ${e.line})') +} + // emit_field_name pushes a field name as a string constant. Like string // literals it goes through a kind-1 relocation so multi-file links rebase it. fn (mut g Gen) emit_field_name(name string) { diff --git a/compiler/lexer.v b/compiler/lexer.v index da667bc..8bc5aa6 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -30,6 +30,7 @@ pub enum TokKind { dotdot dotdotdot kw_fn + kw_struct kw_let kw_if kw_else @@ -254,6 +255,7 @@ fn (mut l Lexer) lex_ident(line int) Tok { lit := l.src[start..l.pos] kind := match lit { 'fn' { TokKind.kw_fn } + 'struct' { TokKind.kw_struct } 'let' { TokKind.kw_let } 'if' { TokKind.kw_if } 'else' { TokKind.kw_else } diff --git a/compiler/parser.v b/compiler/parser.v index ef88c7a..f91bc6f 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -1,8 +1,9 @@ // parser.v — recursive-descent parser for the VuurRaaf language. // // Grammar (informal): -// program := fn* -// fn := 'fn' IDENT '(' [IDENT (',' IDENT)*] ')' block +// program := (struct | fn)* +// struct := 'struct' IDENT '{' [IDENT (',' IDENT)*] '}' +// fn := 'fn' [ '(' IDENT IDENT ')' ] IDENT '(' [IDENT (',' IDENT)*] ')' block // block := '{' stmt* '}' // stmt := 'let' IDENT '=' expr // | IDENT '=' expr @@ -25,8 +26,10 @@ // add := mul (('*'|'/'|'%') mul)* // mul := ('not'|'-') mul | primary // primary := INT | STR | 'true' | 'false' | IDENT ['(' args ')'] | '(' expr ')' -// | '{' [IDENT ':' expr (',' IDENT ':' expr)*] '}' (struct literal) -// postfix := primary ('.' IDENT | '[' expr ']')* (field access, indexing) +// | '{' [IDENT ':' expr (',' IDENT ':' expr)*] '}' (anonymous struct literal) +// | IDENT '{' IDENT ':' expr ... '}' (typed struct literal) +// postfix := primary ('.' IDENT ['(' args ')'] | '[' expr ']')* +// ('.' IDENT '(' ... ')' is a method call; everything else field access) module compiler pub enum ExprKind { @@ -38,6 +41,7 @@ pub enum ExprKind { struct_lit index field + method_call unary binary call @@ -107,17 +111,28 @@ pub mut: line int } -pub struct FnDecl { +// StructDecl is a `struct Name { a, b }` declaration. +pub struct StructDecl { pub mut: name string - params []string - body []Stmt + fields []string line int } +pub struct FnDecl { +pub mut: + name string + recv_name string // method receiver local name ('' for plain functions) + recv_type string // method receiver struct type ('' for plain functions) + params []string + body []Stmt + line int +} + pub struct Program { pub mut: - fns []FnDecl + fns []FnDecl + structs []StructDecl } pub fn parse(toks []Tok) !Program { @@ -168,7 +183,11 @@ fn (mut p Parser) parse_cond() !Expr { fn (mut p Parser) parse_program() !Program { mut prog := Program{} for p.cur().kind != .eof { - prog.fns << p.parse_fn()! + if p.cur().kind == .kw_struct { + prog.structs << p.parse_struct_decl()! + } else { + prog.fns << p.parse_fn()! + } } if prog.fns.len == 0 { return error('no functions found in source') @@ -176,9 +195,46 @@ fn (mut p Parser) parse_program() !Program { return prog } +// parse_struct_decl parses `struct Name { a, b, c }`. +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{} + if p.cur().kind != .rbrace { + for { + fields << p.expect(.ident, 'field name')!.lit + if p.cur().kind == .comma { + p.advance() + continue + } + break + } + } + p.expect(.rbrace, "'}'")! + return StructDecl{ name: name.lit, fields: fields, line: t.line } +} + +// parse_fn parses `fn name(params) { }` or a method `fn (p Type) name(params) { }`. fn (mut p Parser) parse_fn() !FnDecl { fn_tok := p.expect(.kw_fn, "'fn'")! + mut recv_name := '' + mut recv_type := '' + if p.cur().kind == .lparen { + // method: fn (p Type) name(...) + p.advance() + recv_name = p.expect(.ident, 'receiver name')!.lit + recv_type = p.expect(.ident, 'receiver type')!.lit + p.expect(.rparen, "')'")! + } name := p.expect(.ident, 'function name')! + params := p.parse_params()! + body := p.parse_block()! + return FnDecl{ name: name.lit, recv_name: recv_name, recv_type: recv_type, params: params, body: body, line: fn_tok.line } +} + +// parse_params parses `(a, b, c)` — the parameter list of a function. +fn (mut p Parser) parse_params() ![]string { p.expect(.lparen, "'('")! mut params := []string{} if p.cur().kind != .rparen { @@ -192,8 +248,7 @@ fn (mut p Parser) parse_fn() !FnDecl { } } p.expect(.rparen, "')'")! - body := p.parse_block()! - return FnDecl{ name: name.lit, params: params, body: body, line: fn_tok.line } + return params } fn (mut p Parser) parse_block() ![]Stmt { @@ -382,6 +437,12 @@ fn field_node(base Expr, name string, line int) Expr { return Expr{ kind: .field, left: &b, name: name, line: line } } +// method_node builds `base.name(args)`. +fn method_node(recv Expr, name string, args []Expr, line int) Expr { + mut r := recv + return Expr{ kind: .method_call, left: &r, name: name, args: args, line: line } +} + fn (mut p Parser) parse_expr() !Expr { return p.parse_or()! } @@ -466,7 +527,9 @@ fn (mut p Parser) parse_postfix() !Expr { // parse_postfix_tail continues a postfix chain from an already-parsed base. // It takes the base by value (Expr only holds pointers to heap-allocated -// child nodes) and returns the extended chain. +// child nodes) and returns the extended chain. A `.name(` is a method call +// (the receiver is the expression the dot was applied to); `.name` without +// parens is plain field access. fn (mut p Parser) parse_postfix_tail(e Expr) !Expr { mut cur := e for { @@ -480,7 +543,13 @@ fn (mut p Parser) parse_postfix_tail(e Expr) !Expr { if p.cur().kind == .dot { p.advance() name := p.expect(.ident, 'field name')! - cur = field_node(cur, name.lit, cur.line) + f := field_node(cur, name.lit, cur.line) + if p.cur().kind == .lparen { + args := p.parse_args()! + cur = method_node(*f.left, f.name, args, f.line) + } else { + cur = f + } continue } break @@ -530,27 +599,19 @@ fn (mut p Parser) parse_primary() !Expr { return Expr{ kind: .array_lit, elems: elems, line: t.line } } .lbrace { - // struct literal: { name: expr, ... } - p.advance() - mut fields := []StructField{} - if p.cur().kind != .rbrace { - for { - name := p.expect(.ident, 'field name')! - p.expect(.colon, "':'")! - val := p.parse_expr()! - fields << StructField{ name: name.lit, val: val } - if p.cur().kind == .comma { - p.advance() - continue - } - break - } - } - p.expect(.rbrace, "'}'")! - return Expr{ kind: .struct_lit, fields: fields, line: t.line } + // anonymous struct literal: { name: expr, ... } + fields := p.parse_struct_fields()! + return Expr{ kind: .struct_lit, name: '', fields: fields, line: t.line } } .ident { p.advance() + if p.cur().kind == .lbrace && p.looks_like_struct_lit() { + // typed struct literal: Name{ name: expr, ... } — only when the + // brace clearly opens a field list (`{ ident :`), so `if x {` and + // match arms like `x { ... }` still parse as blocks + fields := p.parse_struct_fields()! + return Expr{ kind: .struct_lit, name: t.lit, fields: fields, line: t.line } + } return p.parse_call_or_ident(t)! } .kw_print, .kw_println { @@ -571,6 +632,12 @@ fn (mut p Parser) parse_call_or_ident(t Tok) !Expr { } fn (mut p Parser) parse_call(name Tok) !Expr { + args := p.parse_args()! + return Expr{ kind: .call, name: name.lit, args: args, line: name.line } +} + +// parse_args parses `(e1, e2, ...)` and returns the argument expressions. +fn (mut p Parser) parse_args() ![]Expr { p.expect(.lparen, "'('")! mut args := []Expr{} if p.cur().kind != .rparen { @@ -584,5 +651,35 @@ fn (mut p Parser) parse_call(name Tok) !Expr { } } p.expect(.rparen, "')'")! - return Expr{ kind: .call, name: name.lit, args: args, line: name.line } + return args +} + +// parse_struct_fields parses `{ name: expr, ... }` and returns the fields. +fn (mut p Parser) parse_struct_fields() ![]StructField { + p.expect(.lbrace, "'{'")! + mut fields := []StructField{} + if p.cur().kind != .rbrace { + for { + name := p.expect(.ident, 'field name')! + p.expect(.colon, "':'")! + val := p.parse_expr()! + fields << StructField{ name: name.lit, val: val } + if p.cur().kind == .comma { + p.advance() + continue + } + break + } + } + p.expect(.rbrace, "'}'")! + return fields +} + +// looks_like_struct_lit reports whether the current token (`{`) opens a typed +// struct literal: the tokens after the brace must be `ident :`. +fn (mut p Parser) looks_like_struct_lit() bool { + if p.pos + 2 >= p.toks.len { + return false + } + return p.toks[p.pos + 1].kind == .ident && p.toks[p.pos + 2].kind == .colon }