mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
rewrite interfaces
This commit is contained in:
@ -5,6 +5,7 @@
|
|||||||
module compiler
|
module compiler
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import strings
|
||||||
|
|
||||||
struct CGen {
|
struct CGen {
|
||||||
out os.File
|
out os.File
|
||||||
@ -387,3 +388,32 @@ fn sort_structs(types []Type) []Type {
|
|||||||
}
|
}
|
||||||
return types_sorted
|
return types_sorted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn (v &V) interface_table() string {
|
||||||
|
mut sb := strings.new_builder(100)
|
||||||
|
for _, t in v.table.typesmap {
|
||||||
|
if t.cat != .interface_ {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mut methods := ''
|
||||||
|
for i, gen_type in t.gen_types {
|
||||||
|
methods += '{'
|
||||||
|
for i, method in t.methods {
|
||||||
|
// Cat_speak
|
||||||
|
methods += '${gen_type}_${method.name}'
|
||||||
|
if i < t.methods.len - 1 {
|
||||||
|
methods += ', '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
methods += '}, '
|
||||||
|
// Speaker_Cat_index = 0
|
||||||
|
sb.writeln('int _${t.name}_${gen_type}_index = $i;')
|
||||||
|
}
|
||||||
|
sb.writeln('void* (* ${t.name}_name_table[][$t.methods.len]) = ' +
|
||||||
|
'{ $methods }; ')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return sb.str()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,8 +12,7 @@ const (
|
|||||||
MaxLocalVars = 50
|
MaxLocalVars = 50
|
||||||
)
|
)
|
||||||
|
|
||||||
pub
|
pub struct Fn {
|
||||||
struct Fn {
|
|
||||||
// addr int
|
// addr int
|
||||||
pub:
|
pub:
|
||||||
mut:
|
mut:
|
||||||
@ -560,7 +559,7 @@ fn (p mut Parser) skip_fn_body() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (p Parser) get_linkage_prefix() string {
|
fn (p &Parser) get_linkage_prefix() string {
|
||||||
return if p.pref.ccompiler == 'msvc' && p.attr == 'live' && p.pref.is_so {
|
return if p.pref.ccompiler == 'msvc' && p.attr == 'live' && p.pref.is_so {
|
||||||
'__declspec(dllexport) '
|
'__declspec(dllexport) '
|
||||||
} else if p.attr == 'inline' {
|
} else if p.attr == 'inline' {
|
||||||
@ -718,6 +717,27 @@ fn (p mut Parser) fn_call(f mut Fn, method_ph int, receiver_var, receiver_type s
|
|||||||
// we need to preappend "method(receiver, ...)"
|
// we need to preappend "method(receiver, ...)"
|
||||||
if f.is_method {
|
if f.is_method {
|
||||||
receiver := f.args.first()
|
receiver := f.args.first()
|
||||||
|
if receiver.typ.ends_with('er') {
|
||||||
|
// I absolutely love this syntax
|
||||||
|
// `s.speak()` =>
|
||||||
|
// `((void (*)())(Speaker_name_table[s._interface_idx][1]))(s._object);
|
||||||
|
// where `1` refers to the speak method, since it's the second method
|
||||||
|
// of the Speaker interface
|
||||||
|
t := p.table.find_type(receiver.typ)
|
||||||
|
if t.cat == .interface_ {
|
||||||
|
// Find the index of the method
|
||||||
|
mut idx := 0
|
||||||
|
for i, method in t.methods {
|
||||||
|
if method.name == f.name {
|
||||||
|
idx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.cgen.resetln('')
|
||||||
|
var := p.expr_var.name
|
||||||
|
iname := f.args[0].typ // Speaker
|
||||||
|
p.gen('((void (*)())(${iname}_name_table[${var}._interface_idx][$idx]))(${var}._object)')
|
||||||
|
}
|
||||||
|
}
|
||||||
//println('r=$receiver.typ RT=$receiver_type')
|
//println('r=$receiver.typ RT=$receiver_type')
|
||||||
if receiver.is_mut && !p.expr_var.is_mut {
|
if receiver.is_mut && !p.expr_var.is_mut {
|
||||||
//println('$method_call recv=$receiver.name recv_mut=$receiver.is_mut')
|
//println('$method_call recv=$receiver.name recv_mut=$receiver.is_mut')
|
||||||
@ -747,7 +767,9 @@ fn (p mut Parser) fn_call(f mut Fn, method_ph int, receiver_var, receiver_type s
|
|||||||
// println('calling inst $f.name: $p.cgen.cur_line')
|
// println('calling inst $f.name: $p.cgen.cur_line')
|
||||||
}
|
}
|
||||||
|
|
||||||
p.gen(')')
|
//if !is_interface {
|
||||||
|
p.gen(')')
|
||||||
|
//}
|
||||||
p.calling_c = false
|
p.calling_c = false
|
||||||
if is_comptime_define {
|
if is_comptime_define {
|
||||||
p.cgen.nogen = false
|
p.cgen.nogen = false
|
||||||
@ -757,16 +779,16 @@ fn (p mut Parser) fn_call(f mut Fn, method_ph int, receiver_var, receiver_type s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// for declaration
|
// for declaration
|
||||||
// return an updated Fn object with args[] field set
|
// update the Fn object's args[]
|
||||||
fn (p mut Parser) fn_args(f mut Fn) {
|
fn (p mut Parser) fn_args(f mut Fn) {
|
||||||
p.check(.lpar)
|
p.check(.lpar)
|
||||||
defer { p.check(.rpar) }
|
defer { p.check(.rpar) }
|
||||||
if f.is_interface {
|
if f.is_interface {
|
||||||
int_arg := Var {
|
interface_arg := Var {
|
||||||
typ: f.receiver_typ
|
typ: f.receiver_typ
|
||||||
token_idx: p.cur_tok_index()
|
token_idx: p.cur_tok_index()
|
||||||
}
|
}
|
||||||
f.args << int_arg
|
f.args << interface_arg
|
||||||
}
|
}
|
||||||
// `(int, string, int)`
|
// `(int, string, int)`
|
||||||
// Just register fn arg types
|
// Just register fn arg types
|
||||||
@ -829,7 +851,7 @@ fn (p mut Parser) fn_args(f mut Fn) {
|
|||||||
p.check_and_register_used_imported_type(typ)
|
p.check_and_register_used_imported_type(typ)
|
||||||
if is_mut && is_primitive_type(typ) {
|
if is_mut && is_primitive_type(typ) {
|
||||||
p.error('mutable arguments are only allowed for arrays, maps, and structs.' +
|
p.error('mutable arguments are only allowed for arrays, maps, and structs.' +
|
||||||
'\nreturn values instead: `foo(n mut int)` => `foo(n int) int`')
|
'\nreturn values instead: `fn foo(n mut int) {` => `fn foo(n int) int {`')
|
||||||
}
|
}
|
||||||
for name in names {
|
for name in names {
|
||||||
if is_mut {
|
if is_mut {
|
||||||
@ -916,8 +938,7 @@ fn (p mut Parser) fn_call_args(f mut Fn) {
|
|||||||
ph := p.cgen.add_placeholder()
|
ph := p.cgen.add_placeholder()
|
||||||
// `)` here means that not enough args were provided
|
// `)` here means that not enough args were provided
|
||||||
if p.tok == .rpar {
|
if p.tok == .rpar {
|
||||||
str_args := f.str_args(p.table)// TODO this is C args
|
p.error('not enough arguments in call to `${f.str_for_error()}`')
|
||||||
p.error('not enough arguments in call to `$f.name ($str_args)`')
|
|
||||||
}
|
}
|
||||||
// If `arg` is mutable, the caller needs to provide `mut`:
|
// If `arg` is mutable, the caller needs to provide `mut`:
|
||||||
// `mut numbers := [1,2,3]; reverse(mut numbers);`
|
// `mut numbers := [1,2,3]; reverse(mut numbers);`
|
||||||
@ -951,6 +972,21 @@ fn (p mut Parser) fn_call_args(f mut Fn) {
|
|||||||
p.gen('/*YY f=$f.name arg=$arg.name is_moved=$arg.is_moved*/string_clone(')
|
p.gen('/*YY f=$f.name arg=$arg.name is_moved=$arg.is_moved*/string_clone(')
|
||||||
}
|
}
|
||||||
mut typ := p.bool_expression()
|
mut typ := p.bool_expression()
|
||||||
|
// Register an interface type usage:
|
||||||
|
// fn run(r Animal) { ... }
|
||||||
|
// `run(dog)` adds `Dog` to the `Animal` interface.
|
||||||
|
// This is needed to generate an interface table.
|
||||||
|
if arg.typ.ends_with('er') {
|
||||||
|
t := p.table.find_type(arg.typ)
|
||||||
|
if t.cat == .interface_ {
|
||||||
|
// perform((Speaker) { ._object = &dog,
|
||||||
|
// _interface_idx = _Speaker_Dog_index })
|
||||||
|
p.cgen.set_placeholder(ph, '($arg.typ) { ._object = &')
|
||||||
|
p.gen(', ._interface_idx = _${arg.typ}_${typ}_index} /* i. arg*/')
|
||||||
|
p.table.add_gen_type(arg.typ, typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if clone {
|
if clone {
|
||||||
p.gen(')')
|
p.gen(')')
|
||||||
}
|
}
|
||||||
@ -1077,19 +1113,19 @@ fn (p mut Parser) fn_call_args(f mut Fn) {
|
|||||||
}
|
}
|
||||||
else if is_interface {
|
else if is_interface {
|
||||||
if !got_ptr {
|
if !got_ptr {
|
||||||
p.cgen.set_placeholder(ph, '&')
|
//p.cgen.set_placeholder(ph, '&')
|
||||||
}
|
}
|
||||||
// Pass all interface methods
|
// Pass all interface methods
|
||||||
interface_type := p.table.find_type(arg.typ)
|
//interface_type := p.table.find_type(arg.typ)
|
||||||
for method in interface_type.methods {
|
//for method in interface_type.methods {
|
||||||
p.gen(', ${typ}_${method.name} ')
|
//p.gen(', ${typ}_${method.name} ')
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
// Check for commas
|
// Check for commas
|
||||||
if i < f.args.len - 1 {
|
if i < f.args.len - 1 {
|
||||||
// Handle 0 args passed to varargs
|
// Handle 0 args passed to varargs
|
||||||
if p.tok != .comma && !f.is_variadic {
|
if p.tok != .comma && !f.is_variadic {
|
||||||
p.error('wrong number of arguments for $i,$arg.name fn `$f.name`: expected $f.args.len, but got less')
|
p.error('wrong number of arguments in call to `${f.str_for_error()}`')
|
||||||
}
|
}
|
||||||
if p.tok == .comma && (!f.is_variadic || (f.is_variadic && i < f.args.len-2 )) {
|
if p.tok == .comma && (!f.is_variadic || (f.is_variadic && i < f.args.len-2 )) {
|
||||||
p.check(.comma)
|
p.check(.comma)
|
||||||
@ -1103,7 +1139,7 @@ fn (p mut Parser) fn_call_args(f mut Fn) {
|
|||||||
saved_args << varg_type
|
saved_args << varg_type
|
||||||
}
|
}
|
||||||
if p.tok == .comma {
|
if p.tok == .comma {
|
||||||
p.error('wrong number of arguments for fn `$f.name`: expected $f.args.len, but got more')
|
p.error('wrong number of arguments in call to `${f.str_for_error()}`')
|
||||||
}
|
}
|
||||||
p.check(.rpar)
|
p.check(.rpar)
|
||||||
if f.is_generic {
|
if f.is_generic {
|
||||||
@ -1223,8 +1259,8 @@ fn (p mut Parser) replace_type_params(f &Fn, ti TypeInst) []string {
|
|||||||
fn (p mut Parser) register_vargs_stuct(typ string, len int) string {
|
fn (p mut Parser) register_vargs_stuct(typ string, len int) string {
|
||||||
vargs_struct := 'varg_$typ'
|
vargs_struct := 'varg_$typ'
|
||||||
varg_type := Type{
|
varg_type := Type{
|
||||||
cat: .struct_,
|
cat: .struct_
|
||||||
name: vargs_struct,
|
name: vargs_struct
|
||||||
mod: p.mod
|
mod: p.mod
|
||||||
}
|
}
|
||||||
mut varg_len := len
|
mut varg_len := len
|
||||||
@ -1462,6 +1498,7 @@ fn (f &Fn) str_args(table &Table) string {
|
|||||||
// to all methods:
|
// to all methods:
|
||||||
// fn handle(r Runner) { =>
|
// fn handle(r Runner) { =>
|
||||||
// void handle(void *r, void (*Runner_run)(void*)) {
|
// void handle(void *r, void (*Runner_run)(void*)) {
|
||||||
|
/*
|
||||||
if table.is_interface(arg.typ) {
|
if table.is_interface(arg.typ) {
|
||||||
// First the object (same name as the interface argument)
|
// First the object (same name as the interface argument)
|
||||||
s += ' void* $arg.name'
|
s += ' void* $arg.name'
|
||||||
@ -1477,7 +1514,8 @@ fn (f &Fn) str_args(table &Table) string {
|
|||||||
s += ')'
|
s += ')'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if arg.typ.starts_with('varg_') {
|
*/
|
||||||
|
if arg.typ.starts_with('varg_') {
|
||||||
s += '$arg.typ *$arg.name'
|
s += '$arg.typ *$arg.name'
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -1519,11 +1557,31 @@ fn (fns []Fn) contains(f Fn) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (f Fn) v_fn_module() string {
|
pub fn (f &Fn) v_fn_module() string {
|
||||||
return f.mod
|
return f.mod
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (f Fn) v_fn_name() string {
|
pub fn (f &Fn) v_fn_name() string {
|
||||||
return f.name.replace('${f.mod}__', '')
|
return f.name.replace('${f.mod}__', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (f &Fn) str_for_error() string {
|
||||||
|
// Build the args for the error
|
||||||
|
mut s := ''
|
||||||
|
for i, a in f.args {
|
||||||
|
if i == 0 {
|
||||||
|
if f.is_method {
|
||||||
|
s += a.typ + '.' + f.name + '('
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s += f.name + '('
|
||||||
|
}
|
||||||
|
s += a.typ
|
||||||
|
if i < f.args.len - 1 {
|
||||||
|
s += ', '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s + ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -145,18 +145,23 @@ fn (p mut Parser) gen_handle_option_or_else(_typ, name string, fn_call_ph int) s
|
|||||||
fn types_to_c(types []Type, table &Table) string {
|
fn types_to_c(types []Type, table &Table) string {
|
||||||
mut sb := strings.new_builder(10)
|
mut sb := strings.new_builder(10)
|
||||||
for t in types {
|
for t in types {
|
||||||
if t.cat != .union_ && t.cat != .struct_ && t.cat != .objc_interface {
|
//if t.cat != .union_ && t.cat != .struct_ && t.cat != .objc_interface {
|
||||||
|
if !(t.cat in [.union_, .struct_, .objc_interface, .interface_]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
//if is_atomic {
|
//if is_atomic {
|
||||||
//sb.write('_Atomic ')
|
//sb.write('_Atomic ')
|
||||||
//}
|
//}
|
||||||
if t.cat == .objc_interface {
|
if t.cat == .objc_interface {
|
||||||
sb.writeln('@interface $t.name : $t.parent { @public')
|
sb.writeln('@interface $t.name : $t.parent { @public')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
kind := if t.cat == .union_ {'union'} else {'struct'}
|
kind := if t.cat == .union_ {'union'} else {'struct'}
|
||||||
sb.writeln('$kind $t.name {')
|
sb.writeln('$kind $t.name {')
|
||||||
|
if t.cat == .interface_ {
|
||||||
|
sb.writeln('\tvoid* _object;')
|
||||||
|
sb.writeln('\tint _interface_idx; // int t')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for field in t.fields {
|
for field in t.fields {
|
||||||
sb.write('\t')
|
sb.write('\t')
|
||||||
@ -238,6 +243,11 @@ fn (table mut Table) fn_gen_name(f &Fn) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if f.is_interface {
|
||||||
|
// iname := f.args[0].typ // Speaker
|
||||||
|
// var := p.expr_var.name
|
||||||
|
return ''
|
||||||
|
}
|
||||||
// Avoid name conflicts (with things like abs(), print() etc).
|
// Avoid name conflicts (with things like abs(), print() etc).
|
||||||
// Generate v_abs(), v_print()
|
// Generate v_abs(), v_print()
|
||||||
// TODO duplicate functionality
|
// TODO duplicate functionality
|
||||||
|
@ -297,6 +297,7 @@ pub fn (v mut V) compile() {
|
|||||||
def.writeln('\nstring _STR(const char*, ...);\n')
|
def.writeln('\nstring _STR(const char*, ...);\n')
|
||||||
def.writeln('\nstring _STR_TMP(const char*, ...);\n')
|
def.writeln('\nstring _STR_TMP(const char*, ...);\n')
|
||||||
def.writeln(cgen.fns.join_lines()) // fn definitions
|
def.writeln(cgen.fns.join_lines()) // fn definitions
|
||||||
|
def.writeln(v.interface_table())
|
||||||
} $else {
|
} $else {
|
||||||
def.writeln(v.type_definitions())
|
def.writeln(v.type_definitions())
|
||||||
}
|
}
|
||||||
|
@ -697,8 +697,8 @@ fn (p mut Parser) check_string() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn (p mut Parser) check_not_reserved () {
|
fn (p mut Parser) check_not_reserved () {
|
||||||
if Reserved_Types[p.lit] {
|
if Reserved_Types[p.lit] {
|
||||||
p.error('`$p.lit` can\'t be used as name')
|
p.error('`$p.lit` can\'t be used as name')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2053,6 +2053,7 @@ fn (p mut Parser) indot_expr() string {
|
|||||||
return typ
|
return typ
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// { user | name: 'new name' }
|
||||||
fn (p mut Parser) assoc() string {
|
fn (p mut Parser) assoc() string {
|
||||||
// println('assoc()')
|
// println('assoc()')
|
||||||
p.next()
|
p.next()
|
||||||
|
@ -222,12 +222,8 @@ fn (p mut Parser) struct_decl() {
|
|||||||
p.fgenln('')
|
p.fgenln('')
|
||||||
}
|
}
|
||||||
p.check(.rcbr)
|
p.check(.rcbr)
|
||||||
if !is_c {
|
if !is_c && !did_gen_something && p.first_pass() {
|
||||||
if !did_gen_something {
|
p.table.add_field(typ.name, '', 'EMPTY_STRUCT_DECLARATION', false, '', .private)
|
||||||
if p.first_pass() {
|
|
||||||
p.table.add_field(typ.name, '', 'EMPTY_STRUCT_DECLARATION', false, '', .private)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
p.fgenln('\n')
|
p.fgenln('\n')
|
||||||
}
|
}
|
||||||
|
@ -538,16 +538,14 @@ fn (t &Type) find_method(name string) ?Fn {
|
|||||||
return none
|
return none
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
fn (table mut Table) add_gen_type(type_name, gen_type string) {
|
||||||
// TODO
|
mut t := table.typesmap[type_name]
|
||||||
fn (t mutt Type) add_gen_type(type_name string) {
|
if gen_type in t.gen_types {
|
||||||
// println('add_gen_type($s)')
|
|
||||||
if t.gen_types.contains(type_name) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.gen_types << type_name
|
t.gen_types << gen_type
|
||||||
|
table.typesmap[type_name] = t
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
fn (p &Parser) find_type(name string) Type {
|
fn (p &Parser) find_type(name string) Type {
|
||||||
typ := p.table.find_type(name)
|
typ := p.table.find_type(name)
|
||||||
@ -576,7 +574,7 @@ fn (p mut Parser) check_types2(got_, expected_ string, throw bool) bool {
|
|||||||
if p.pref.translated {
|
if p.pref.translated {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if got == expected {
|
if got == expected {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -728,7 +726,8 @@ fn (p mut Parser) satisfies_interface(interface_name, _typ string, throw bool) b
|
|||||||
for method in int_typ.methods {
|
for method in int_typ.methods {
|
||||||
if !typ.has_method(method.name) {
|
if !typ.has_method(method.name) {
|
||||||
// if throw {
|
// if throw {
|
||||||
p.error('Type "$_typ" doesn\'t satisfy interface "$interface_name" (method "$method.name" is not implemented)')
|
p.error('type `$_typ` doesn\'t satisfy interface ' +
|
||||||
|
'`$interface_name` (method `$method.name` is not implemented)')
|
||||||
// }
|
// }
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,16 @@ struct Dog {
|
|||||||
breed string
|
breed string
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (d Dog) speak() {
|
struct Cat {
|
||||||
println('dog.speak()')
|
breed string
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (d Dog) name() string {
|
|
||||||
return 'old gray'
|
fn (d Cat) name() string { return 'Cat' }
|
||||||
}
|
fn (d Cat) speak() { println('meow') }
|
||||||
|
|
||||||
|
fn (d Dog) speak() { println('woof') }
|
||||||
|
fn (d Dog) name() string { return 'Dog'}
|
||||||
|
|
||||||
interface Speaker {
|
interface Speaker {
|
||||||
name() string
|
name() string
|
||||||
@ -16,10 +19,14 @@ interface Speaker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Speak2er {
|
interface Speak2er {
|
||||||
speak()
|
|
||||||
name() string
|
name() string
|
||||||
|
speak()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Foo {
|
||||||
|
speaker Speaker
|
||||||
|
}
|
||||||
|
|
||||||
fn perform_speak(s Speaker) bool {
|
fn perform_speak(s Speaker) bool {
|
||||||
s.speak()
|
s.speak()
|
||||||
return true
|
return true
|
||||||
@ -28,5 +35,11 @@ fn perform_speak(s Speaker) bool {
|
|||||||
fn test_perform_speak() {
|
fn test_perform_speak() {
|
||||||
d := Dog{}
|
d := Dog{}
|
||||||
assert perform_speak(d)
|
assert perform_speak(d)
|
||||||
|
cat := Cat{}
|
||||||
|
assert perform_speak(cat)
|
||||||
|
f := Foo {
|
||||||
|
//speaker: d
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user