From 34485cd6c8581ab3f3fabece7eee027227dc02fe Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Sat, 18 Apr 2026 15:41:36 +0900 Subject: [PATCH] feat(oop): METHOD ... INLINE and MESSAGE handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Harbour's inline-method sugar was parsed but the body was skipped, leaving any `METHOD X() INLINE expr` declaration registered in the class vtable with no matching HB__X function — link error at build time. Parser: MethodDecl gains an InlineBody Expr field. parseClassMethodDecl captures the expression after INLINE instead of skipping to EOL. New parseMessageDecl handles `MESSAGE [(params)] INLINE expr` and returns the same MethodDecl shape. Codegen: emitClassDecl walks members a second time after the class registration init block and emits emitInlineMethodBody for each IsInline method — a Frame(nParams, 0) + emitExpr(InlineBody) + RetValue function. curMethodClass is bound so ::super: inside an inline body still resolves. Tested (/tmp/test_inline.prg): all four patterns — bare INLINE, MESSAGE INLINE, INLINE with params, INLINE reading ::field — produce expected values. FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS. Co-Authored-By: Claude Opus 4.7 (1M context) --- compiler/ast/ast.go | 1 + compiler/gengo/gen_class.go | 46 ++++++++++++++++++++++ compiler/parser/parser.go | 77 ++++++++++++++++++++++++++++++++----- 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/compiler/ast/ast.go b/compiler/ast/ast.go index fbd3b1a..99ae418 100644 --- a/compiler/ast/ast.go +++ b/compiler/ast/ast.go @@ -177,6 +177,7 @@ type MethodDecl struct { ClassName string // METHOD name CLASS classname (standalone) Params []*ParamDecl IsInline bool // INLINE method + InlineBody Expr // inline expression body — `RETURN ` equivalent IsSetGet bool // METHOD name(x) SETGET — getter if no arg, setter if arg IsAccess bool // ACCESS name METHOD getterName IsAssign bool // ASSIGN name METHOD setterName diff --git a/compiler/gengo/gen_class.go b/compiler/gengo/gen_class.go index f9570f8..e6598e1 100644 --- a/compiler/gengo/gen_class.go +++ b/compiler/gengo/gen_class.go @@ -74,6 +74,18 @@ func (g *Generator) emitClassDecl(cls *ast.ClassDecl) { g.writeln("}") g.writeln("") + // Emit function bodies for INLINE methods — the class-body form + // `METHOD X() INLINE expr` / `MESSAGE X INLINE expr` carries its + // own body, unlike plain METHOD declarations which expect a + // standalone `METHOD X() CLASS Foo` implementation elsewhere. + for _, m := range cls.Members { + md, ok := m.(*ast.MethodDecl) + if !ok || !md.IsInline || md.InlineBody == nil { + continue + } + g.emitInlineMethodBody(className, md) + } + // Also need a constructor function: Person() returns new object // This is called as Person():New(...) g.writeln(fmt.Sprintf("func HB_%s_CTOR(t *hbrt.Thread) {", className)) @@ -89,6 +101,40 @@ func (g *Generator) emitClassDecl(cls *ast.ClassDecl) { // Constructor symbol already added in Generate() symbol collection phase } +// emitInlineMethodBody generates the HB__ function for +// an INLINE-declared method: the body is the single expression parsed +// after the INLINE keyword, evaluated and returned. Params bind to +// locals 1..N so the inline expression can reference them. +func (g *Generator) emitInlineMethodBody(className string, md *ast.MethodDecl) { + methodName := strings.ToUpper(md.Name) + goFuncName := fmt.Sprintf("HB_%s_%s", className, methodName) + nParams := len(md.Params) + + g.writeln(fmt.Sprintf("func %s(t *hbrt.Thread) {", goFuncName)) + g.indent++ + g.writeln(fmt.Sprintf("t.Frame(%d, 0)", nParams)) + g.writeln("defer t.EndProc()") + + // Param name → local index map so the inline expr can reference them. + localMap := make(localMap) + for i, p := range md.Params { + localMap[strings.ToUpper(p.Name)] = i + 1 + } + prevLocals := g.curLocals + prevCls := g.curMethodClass + g.curLocals = localMap + g.curMethodClass = className + + g.emitExpr(md.InlineBody) + g.writeln("t.RetValue()") + + g.curLocals = prevLocals + g.curMethodClass = prevCls + g.indent-- + g.writeln("}") + g.writeln("") +} + // emitMethodDeclStandalone generates a standalone METHOD ... CLASS ... implementation. func (g *Generator) emitMethodDeclStandalone(md *ast.MethodDecl) { if md.ClassName == "" { diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 875cc98..8ac8d29 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -610,8 +610,14 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl { } else { p.skipToEndOfLine() } + } else if upper == "MESSAGE" { + // MESSAGE [(params)] INLINE + // Harbour sugar for an inline method. Emit as MethodDecl so + // emitClassDecl routes it through AddMethod and the inline + // body emitter generates the HB__ function. + members = append(members, p.parseMessageDecl()) } else if upper == "ON" || upper == "OPERATOR" || upper == "DESTRUCTOR" || - upper == "DELEGATE" || upper == "ERROR" || upper == "MESSAGE" || + upper == "DELEGATE" || upper == "ERROR" || upper == "VIRTUAL" || upper == "DEFERRED" { // ON ERROR, OPERATOR "+" ARG, DESTRUCTOR, DELEGATE — skip to EOL p.skipToEndOfLine() @@ -740,21 +746,74 @@ func (p *Parser) parseClassMethodDecl() *ast.MethodDecl { p.skipToEndOfLine() } - // Skip INLINE + rest of line (METHOD ... INLINE expr) - p.skipClassInline() + // INLINE expression — parse as the method body instead of skipping. + // Harbour semantics: `METHOD X() INLINE expr` is sugar for a method + // whose body is `RETURN expr`. + isInline := false + var inlineBody ast.Expr + if p.current.Kind == token.INLINE_KW || + (p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") { + p.advance() // consume INLINE + isInline = true + inlineBody = p.parseExpr() + } p.expectEndOfStmt() return &ast.MethodDecl{ - MethodPos: methodPos, - Name: name, - Params: params, - IsSetGet: isSetGet, - EndPos: methodPos, + MethodPos: methodPos, + Name: name, + Params: params, + IsSetGet: isSetGet, + IsInline: isInline, + InlineBody: inlineBody, + EndPos: methodPos, } } -// skipClassInline skips INLINE keyword and the rest of the line (used in CLASS body) +// parseMessageDecl parses `MESSAGE [(params)] INLINE ` in +// a CLASS body and returns a MethodDecl. Harbour semantics: a MESSAGE +// handler is invoked like a method and behaves identically — the form +// exists mostly for readability of "this ident is really a message +// handler, not a regular method". Without INLINE the handler is +// declaration-only (the real body arrives as a separate +// `METHOD () CLASS X` implementation). +func (p *Parser) parseMessageDecl() *ast.MethodDecl { + msgPos := p.current.Pos + p.advance() // MESSAGE + + name := p.expectMethodName().Literal + + var params []*ast.ParamDecl + if p.match(token.LPAREN) { + params = p.parseParamList() + p.expect(token.RPAREN) + } + + isInline := false + var inlineBody ast.Expr + if p.current.Kind == token.INLINE_KW || + (p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") { + p.advance() + isInline = true + inlineBody = p.parseExpr() + } + + p.expectEndOfStmt() + + return &ast.MethodDecl{ + MethodPos: msgPos, + Name: name, + Params: params, + IsInline: isInline, + InlineBody: inlineBody, + EndPos: msgPos, + } +} + +// skipClassInline skips INLINE keyword and the rest of the line. +// Used by ACCESS/ASSIGN decls where inline body handling hasn't been +// wired up yet (falls back to pre-INLINE-capture behaviour). func (p *Parser) skipClassInline() { if p.current.Kind == token.INLINE_KW || (p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {