0

Add conditional result keyword to mojo parser/lexer

This allows developers to specify result<T, E> in the IDL. E.g.:
Method() => result<bool, bool>.

This does not prevent developers from using 'result' as an identifier in
parameter lists. E.g.: Echo(int result) => (int result).

We rely on conditional lexing and embedded actions to allows context
sensitive tokenization for the 'result' string. Under the 'responsetype'
lex state, the lexer will recognize 'result' as a keyword. In any other
lex state, 'result' results in a NAME token.

This is not wired up to anything atm. The RESULT token is simply
swallowed and treated as an empty response atm.

Change-Id: I0c955db6f7be12a5b87d9cdf77423766709d7b44
Bug: 40841428
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6289176
Reviewed-by: Alex Gough <ajgo@chromium.org>
Commit-Queue: Fred Shih <ffred@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1423387}
This commit is contained in:
Fred Shih
2025-02-21 13:25:43 -08:00
committed by Chromium LUCI CQ
parent adafea8932
commit 925d9a55ec
7 changed files with 298 additions and 5 deletions

@ -0,0 +1,11 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
module golden;
struct ResultTestError {
};
interface ResultInterface {
Method(bool a) => result<bool, ResultTestError>;
};

@ -0,0 +1,7 @@
// results.test-mojom-converters.ts is auto generated by mojom_bindings_generator.py, do not edit
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {mojo} from '//resources/mojo/mojo/public/js/bindings.js';

@ -0,0 +1,203 @@
// results.test-mojom-webui.ts is auto generated by mojom_bindings_generator.py, do not edit
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {mojo} from '//resources/mojo/mojo/public/js/bindings.js';
export class ResultInterfacePendingReceiver implements
mojo.internal.interfaceSupport.PendingReceiver {
handle: mojo.internal.interfaceSupport.Endpoint;
constructor(handle: MojoHandle|mojo.internal.interfaceSupport.Endpoint) {
this.handle = mojo.internal.interfaceSupport.getEndpointForReceiver(handle);
}
bindInBrowser(scope: string = 'context') {
mojo.internal.interfaceSupport.bind(
this.handle,
'golden.ResultInterface',
scope);
}
}
export interface ResultInterfaceInterface {
method(a: boolean): void;
}
export class ResultInterfaceRemote implements ResultInterfaceInterface {
private proxy: mojo.internal.interfaceSupport.InterfaceRemoteBase<ResultInterfacePendingReceiver>;
$: mojo.internal.interfaceSupport.InterfaceRemoteBaseWrapper<ResultInterfacePendingReceiver>;
onConnectionError: mojo.internal.interfaceSupport.ConnectionErrorEventRouter;
constructor(
handle?: MojoHandle|mojo.internal.interfaceSupport.Endpoint) {
this.proxy =
new mojo.internal.interfaceSupport.InterfaceRemoteBase(
ResultInterfacePendingReceiver, handle);
this.$ = new mojo.internal.interfaceSupport.InterfaceRemoteBaseWrapper(this.proxy);
this.onConnectionError = this.proxy.getConnectionErrorEventRouter();
}
method(
a: boolean): void {
this.proxy.sendMessage(
0,
ResultInterface_Method_ParamsSpec.$,
null,
[
a
]);
}
};
/**
* An object which receives request messages for the ResultInterface
* mojom interface. Must be constructed over an object which implements that
* interface.
*/
export class ResultInterfaceReceiver {
private helper_internal_: mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal<ResultInterfaceRemote>;
$: mojo.internal.interfaceSupport.InterfaceReceiverHelper<ResultInterfaceRemote>;
onConnectionError: mojo.internal.interfaceSupport.ConnectionErrorEventRouter;
constructor(impl: ResultInterfaceInterface) {
this.helper_internal_ = new mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal(
ResultInterfaceRemote);
this.$ = new mojo.internal.interfaceSupport.InterfaceReceiverHelper(this.helper_internal_);
this.helper_internal_.registerHandler(
0,
ResultInterface_Method_ParamsSpec.$,
null,
impl.method.bind(impl));
this.onConnectionError = this.helper_internal_.getConnectionErrorEventRouter();
}
}
export class ResultInterface {
static get $interfaceName(): string {
return "golden.ResultInterface";
}
/**
* Returns a remote for this interface which sends messages to the browser.
* The browser must have an interface request binder registered for this
* interface and accessible to the calling document's frame.
*/
static getRemote(): ResultInterfaceRemote {
let remote = new ResultInterfaceRemote;
remote.$.bindNewPipeAndPassReceiver().bindInBrowser();
return remote;
}
}
/**
* An object which receives request messages for the ResultInterface
* mojom interface and dispatches them as callbacks. One callback receiver exists
* on this object for each message defined in the mojom interface, and each
* receiver can have any number of listeners added to it.
*/
export class ResultInterfaceCallbackRouter {
private helper_internal_: mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal<ResultInterfaceRemote>;
$: mojo.internal.interfaceSupport.InterfaceReceiverHelper<ResultInterfaceRemote>;
router_: mojo.internal.interfaceSupport.CallbackRouter;
method: mojo.internal.interfaceSupport.InterfaceCallbackReceiver;
onConnectionError: mojo.internal.interfaceSupport.ConnectionErrorEventRouter;
constructor() {
this.helper_internal_ = new mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal(
ResultInterfaceRemote);
this.$ = new mojo.internal.interfaceSupport.InterfaceReceiverHelper(this.helper_internal_);
this.router_ = new mojo.internal.interfaceSupport.CallbackRouter;
this.method =
new mojo.internal.interfaceSupport.InterfaceCallbackReceiver(
this.router_);
this.helper_internal_.registerHandler(
0,
ResultInterface_Method_ParamsSpec.$,
null,
this.method.createReceiverHandler(false /* expectsResponse */));
this.onConnectionError = this.helper_internal_.getConnectionErrorEventRouter();
}
/**
* @param id An ID returned by a prior call to addListener.
* @return True iff the identified listener was found and removed.
*/
removeListener(id: number): boolean {
return this.router_.removeListener(id);
}
}
export const ResultTestErrorSpec: { $: mojo.internal.MojomType } =
{ $: {} as unknown as mojo.internal.MojomType };
export const ResultInterface_Method_ParamsSpec: { $: mojo.internal.MojomType } =
{ $: {} as unknown as mojo.internal.MojomType };
export interface ResultTestErrorMojoType {
}
export type ResultTestError = ResultTestErrorMojoType;
mojo.internal.Struct<ResultTestErrorMojoType>(
ResultTestErrorSpec.$,
'ResultTestError',
[
],
[[0, 8],]);
export interface ResultInterface_Method_ParamsMojoType {
a: boolean;
}
export type ResultInterface_Method_Params = ResultInterface_Method_ParamsMojoType;
mojo.internal.Struct<ResultInterface_Method_ParamsMojoType>(
ResultInterface_Method_ParamsSpec.$,
'ResultInterface_Method_Params',
[
mojo.internal.StructField<ResultInterface_Method_ParamsMojoType, boolean>(
'a', 0,
0,
mojo.internal.Bool,
false,
false /* nullable */,
0,
undefined,
undefined,
),
],
[[0, 16],]);

@ -103,9 +103,21 @@ class Lexer:
'COMMA',
'PIPE', # |
'AMPERSAND', # &
'DOT' # , .
'DOT', # , .
# Conditional keywords
'RESULT',
)
states = [
# Lex state to parse method response type. This is because we use
# 'result' as a keyword when declaring the return type of a method.
# E.g.: FooMethod() => result<T, E>
# This state is needed to disambiguate the keyword from an identifier in
# the context of a response return type.
('responsetype', 'inclusive'),
]
##
## Regexes for use in tokens
##
@ -197,6 +209,9 @@ class Lexer:
t_STRING_LITERAL = string_literal
# Conditional keywords
t_responsetype_RESULT = r'result'
# The following floating and integer constants are defined as
# functions to impose a strict order (otherwise, decimal
# is placed before the others because its regex is longer,

@ -159,9 +159,17 @@ class LexerTest(unittest.TestCase):
_MakeLexToken("COMMA", ","))
self.assertEqual(self._SingleTokenForInput("."), _MakeLexToken("DOT", "."))
def _TokensForInput(self, input_string):
def testConditionalTokens(self):
self.assertEqual(self._SingleTokenForInput("result"),
_MakeLexToken("NAME", "result"))
self.assertEqual(self._SingleTokenForInput("result", "responsetype"),
_MakeLexToken("RESULT", "result"))
def _TokensForInput(self, input_string, state=None):
"""Gets a list of tokens for the given input string."""
lexer = self._zygote_lexer.clone()
if state:
lexer.begin(state)
lexer.input(input_string)
rv = []
while True:
@ -170,10 +178,10 @@ class LexerTest(unittest.TestCase):
return rv
rv.append(tok)
def _SingleTokenForInput(self, input_string):
def _SingleTokenForInput(self, input_string, state=None):
"""Gets the single token for the given input string. (Raises an exception if
the input string does not result in exactly one token.)"""
toks = self._TokensForInput(input_string)
toks = self._TokensForInput(input_string, state)
assert len(toks) == 1
return toks[0]

@ -303,7 +303,30 @@ class Parser:
p[0] = None
def p_response_2(self, p):
"""response : RESPONSE LPAREN parameter_list RPAREN"""
"""response : response_push_state RESPONSE response_type"""
p[0] = p[3]
# Embedded action to disambiguate 'result' keyword from an name. A push
# embedded action should be followed by a pop action before the response
# type is reduced. Otherwise, 'result' will not be allowed as an identifier
# name.
def p_response_push_state(self, p):
"""response_push_state :"""
p.lexer.push_state('responsetype')
p[0] = None
def p_response_pop_state(self, p):
"""response_pop_state :"""
p.lexer.pop_state()
p[0] = None
def p_response_type_1(self, p):
"""response_type : RESULT response_pop_state LANGLE typename COMMA typename RANGLE"""
# TODO(crbug.com/40841428): implement result type.
p[0] = None
def p_response_type_2(self, p):
"""response_type : LPAREN response_pop_state parameter_list RPAREN"""
p[0] = p[3]
def p_method(self, p):

@ -920,6 +920,32 @@ class ParserTest(unittest.TestCase):
])
self.assertEqual(parser.Parse(source3, "my_file.mojom"), expected3)
source4 = """
interface MyInterface {
MyMethod(string a) => result<bool, bool>;
};
"""
expected4 = ast.Mojom(
None,
ast.ImportList(),
[
ast.Interface(
ast.Name('MyInterface'),
None,
ast.InterfaceBody(
ast.Method(
ast.Name('MyMethod'),
None,
None,
ast.ParameterList(
ast.Parameter(
ast.Name('a'), None, None,
ast.Typename(ast.Identifier('string')))),
# TODO(crbug.com/40841428): put in an actual return.
None)))
])
self.assertEqual(parser.Parse(source4, "my_file.mojom"), expected4)
def testInvalidMethods(self):
"""Tests that invalid method declarations are correctly detected."""