Skip to content

Commit 049b95d

Browse files
committed
The Optimistic Runtime Parser (#289)
This is a re-write of the runtime parser. It supports Fluent Syntax 0.7, runs against the reference fixtures, has half the lines of code, and is as fast in SpiderMonkey as the old one (and slightly faster in V8). Goals 1. Support 100% of Fluent Syntax 0.7. This includes the indentation relaxation, dropping tabs and CR as syntax whitespace, normalizing new lines to LF, and only allowing numbers and identifiers as variant keys. 2. Maintain good performance. The parser is used in performance-critical code paths. Back in the days of Firefox OS it had to be both fast _and_ produce tightly packed results so that translations don't take up too much space on the device. I think the storage requirements can be relaxed these days. 3. Write code which will be easy to maintain in the future. The parser was first written even before Fluent branched off from L20n. It's seen many changes and additions over the last two years. As new features accrued it became hard to maintain it and also to keep track of all known bugs. My goal for the re-write was not only to clean it up but also to define the conformance story for the future and to improve the testing infrastructure. Design The parser focuses on minimizing the number of false negatives at the expense of increasing the risk of false positives. In other words, it aims at parsing _valid_ Fluent messages with a success rate of 100%, but it may also parse some invalid messages which the reference parser would reject. The parser doesn't perform any validation and may produce entries which wouldn't make sense in the real world. For best results users are advised to validate translations with the fluent-syntax parser pre-runtime. The main parser loop iterates over the beginnings of messages and terms. This is to efficiently skip over comments (which have no use on runtime), and to recover from errors. When a fatal error is encountered, the parser instantly bails out of the currently-parsed message and moves on to the next one. Errors are discarded and are not visible to the users of `FluentResource`. The do carry a minimal description of what went wrong which may be useful when reading the code and for debugging, though. The parser makes an extensive use of sticky regexes which can be anchored to any offset of the source string without slicing it. In some places, it's easier to just check the character currently at the cursor, so it does a fair share of that, too. Conformance My original plan was to base the parser on the EBNF and only parse well-formed syntax. In this PR, I went for something a bit wider than that: a superset of well-formed syntax. The main deviation from the EBNF is related to parsing `VariantExpressions` and `CallExpressions`. The EBNF verifies that the they are called on `Terms` and `Functions` respectively. The optimistic parser doesn't differentiate between `Messages`, `Terms` and `Functions`. I decided to implement it this way because this code might soon change anyways (see projectfluent/fluent#176). Another deviation is that the parser treats commas in argument lists as whitespace, similar to how Clojure treats them in sequence lists. I might suggest we upstream this in the spec, too, because it makes the implementation of args lists _much_ simpler. I based this PR on top of the `zeroseven` branch. The `fluent-syntax` parser already supports Syntax 0.7 and passes the [reference fixtures](https://github.com/projectfluent/fluent/tree/master/test/fixtures). This made it possible to also turn on the reference testing in the runtime parser, too. `make fixtures` creates the parsed results for all reference fixtures; for now they must be verified manually before they're committed. `make test` can be used in development to assert that the output of the runtime parser still matches the committed one.
1 parent 52e1854 commit 049b95d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+2754
-1609
lines changed

eslint_src.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
"no-mixed-spaces-and-tabs": 2,
6969
"no-tabs": 2,
7070
"prefer-arrow-callback": 1,
71-
"prefer-const": 2,
7271
"prefer-template": 2,
7372
"prefer-spread": 2,
7473
"quotes": [

fluent/makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,24 @@ clean:
2929
@echo -e " $(OK) clean"
3030

3131
BEHAVIOR_FTL := $(wildcard ../fluent-syntax/test/fixtures_behavior/*.ftl)
32+
REFERENCE_FTL := $(wildcard ../fluent-syntax/test/fixtures_reference/*.ftl)
3233
STRUCTURE_FTL := $(wildcard ../fluent-syntax/test/fixtures_structure/*.ftl)
3334
BEHAVIOR_JSON := $(BEHAVIOR_FTL:../fluent-syntax/test/fixtures_behavior/%.ftl=test/fixtures_behavior/%.json)
35+
REFERENCE_JSON := $(REFERENCE_FTL:../fluent-syntax/test/fixtures_reference/%.ftl=test/fixtures_reference/%.json)
3436
STRUCTURE_JSON := $(STRUCTURE_FTL:../fluent-syntax/test/fixtures_structure/%.ftl=test/fixtures_structure/%.json)
3537

36-
fixtures: $(BEHAVIOR_JSON) $(STRUCTURE_JSON)
38+
fixtures: $(BEHAVIOR_JSON) $(REFERENCE_JSON) $(STRUCTURE_JSON)
3739

3840
.PHONY: $(BEHAVIOR_JSON)
3941
$(BEHAVIOR_JSON): test/fixtures_behavior/%.json: ../fluent-syntax/test/fixtures_behavior/%.ftl
4042
@node test/fixtures_behavior/make_fixtures.js -- $< > $@
4143
@echo -e " $(OK) $@"
4244

45+
.PHONY: $(REFERENCE_JSON)
46+
$(REFERENCE_JSON): test/fixtures_reference/%.json: ../fluent-syntax/test/fixtures_reference/%.ftl
47+
@../tools/parse.js --runtime --silent $< > $@
48+
@echo -e " $(OK) $@"
49+
4350
.PHONY: $(STRUCTURE_JSON)
4451
$(STRUCTURE_JSON): test/fixtures_structure/%.json: ../fluent-syntax/test/fixtures_structure/%.ftl
4552
@../tools/parse.js --runtime --silent $< > $@

fluent/src/builtins.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* `FluentType`. Functions must return `FluentType` objects as well.
1212
*/
1313

14-
import { FluentNumber, FluentDateTime } from "./types";
14+
import { FluentNumber, FluentDateTime } from "./types.js";
1515

1616
export default {
1717
"NUMBER": ([arg], opts) =>

fluent/src/bundle.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import resolve from "./resolver";
2-
import FluentResource from "./resource";
1+
import resolve from "./resolver.js";
2+
import FluentResource from "./resource.js";
33

44
/**
55
* Message bundles are single-language stores of translations. They are
@@ -138,7 +138,8 @@ export default class FluentBundle {
138138
* @returns {Array<Error>}
139139
*/
140140
addResource(res) {
141-
const errors = res.errors.slice();
141+
const errors = [];
142+
142143
for (const [id, value] of res) {
143144
if (id.startsWith("-")) {
144145
// Identifiers starting with a dash (-) define terms. Terms are private
@@ -196,16 +197,16 @@ export default class FluentBundle {
196197
return this._transform(message);
197198
}
198199

199-
// optimize simple-string entities with attributes
200-
if (typeof message.val === "string") {
201-
return this._transform(message.val);
202-
}
203-
204200
// optimize entities with null values
205-
if (message.val === undefined) {
201+
if (message === null || message.value === null) {
206202
return null;
207203
}
208204

205+
// optimize simple-string entities with attributes
206+
if (typeof message.value === "string") {
207+
return this._transform(message.value);
208+
}
209+
209210
return resolve(this, args, message, errors);
210211
}
211212

fluent/src/error.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default class FluentError extends Error {}

fluent/src/index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
*
88
*/
99

10-
export { default as FluentBundle } from "./bundle";
11-
export { default as FluentResource } from "./resource";
12-
export { FluentType, FluentNumber, FluentDateTime } from "./types";
10+
export { default as FluentBundle } from "./bundle.js";
11+
export { default as FluentResource } from "./resource.js";
12+
export { default as FluentError } from "./error.js";
13+
export { FluentType, FluentNumber, FluentDateTime } from "./types.js";
1314

14-
export { ftl } from "./util";
15+
export { ftl } from "./util.js";

0 commit comments

Comments
 (0)