feat(oop): METHOD ... INLINE <expr> and MESSAGE handlers

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_<CLASS>_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 <name> [(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) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 15:41:36 +09:00
parent 3a56bd321a
commit 34485cd6c8
3 changed files with 115 additions and 9 deletions

View File

@@ -610,8 +610,14 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl {
} else {
p.skipToEndOfLine()
}
} else if upper == "MESSAGE" {
// MESSAGE <name> [(params)] INLINE <expr>
// Harbour sugar for an inline method. Emit as MethodDecl so
// emitClassDecl routes it through AddMethod and the inline
// body emitter generates the HB_<CLASS>_<NAME> 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 <name> [(params)] INLINE <expr>` 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 <name>() 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") {