Workflows

This commit is contained in:
allexanderbergmns
2026-08-24 17:30:50 +02:00
parent 265e667912
commit 31d1c20790
4 changed files with 286 additions and 36 deletions
+66
View File
@@ -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
+89 -4
View File
@@ -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 <Type>.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) {
+2
View File
@@ -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 }
+124 -27
View File
@@ -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,9 +111,19 @@ pub mut:
line int
}
// StructDecl is a `struct Name { a, b }` declaration.
pub struct StructDecl {
pub mut:
name string
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
@@ -118,6 +132,7 @@ pub mut:
pub struct Program {
pub mut:
fns []FnDecl
structs []StructDecl
}
pub fn parse(toks []Tok) !Program {
@@ -168,17 +183,58 @@ fn (mut p Parser) parse_cond() !Expr {
fn (mut p Parser) parse_program() !Program {
mut prog := Program{}
for p.cur().kind != .eof {
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')
}
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
}