mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 14:17:18 +00:00
Workflows
This commit is contained in:
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+129
-32
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user