exprforge 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +29 -0
  2. package/expr.js +11 -0
  3. package/fn.js +55 -3
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -355,6 +355,35 @@ deferred-to-`collectLets` behavior every hand-built `letIn`/`letChain`
355
355
  already has. A `fn` body with no `let` statements at all is just
356
356
  `return expr;`, equivalent to a bare `expr` call.
357
357
 
358
+ ### Optional signature line
359
+
360
+ Writing `name`/`params` separately, next to the body, is fine for a
361
+ one-off — but `fn` can carry them too, with a leading `name(params):`
362
+ line:
363
+
364
+ ```js
365
+ const { fn, emitAll, evaluate } = require("exprforge");
366
+
367
+ const normalize2 = fn`
368
+ normalize2(x, y):
369
+ let mag = sqrt(x^2 + y^2);
370
+ return { nx: x / mag, ny: y / mag };
371
+ `;
372
+ // normalize2 is now the full {name, params, body} shape directly --
373
+ // no wrapping object needed.
374
+
375
+ evaluate(normalize2, [3, 4]); // { nx: 0.6, ny: 0.8 }
376
+ emitAll(normalize2).rust.source; // ready to use immediately
377
+ ```
378
+
379
+ This changes `fn`'s return type based on what you wrote, deliberately:
380
+ no signature → a bare `Node`, exactly as above and fully backward
381
+ compatible; a signature present → the full `{name, params, body}`
382
+ object. `let`/`return` still can't be used as a function name — a
383
+ signature is told apart from a statement by the same rule that tells
384
+ `let`/`return` apart from any other identifier, so naming a function
385
+ `let` just parses as (and fails as) a `let` statement instead.
386
+
358
387
  ## Printing an AST back out, and a native evaluator
359
388
 
360
389
  Two things that fall out of `fn` existing: `emitters.expr` is a real,
package/expr.js CHANGED
@@ -136,6 +136,17 @@ class Parser {
136
136
  return this.tokens[this.i];
137
137
  }
138
138
 
139
+ // One token beyond peek() -- always safe, since `tokens` always ends
140
+ // with an EOF token appended once by expr()/fn() before the parser
141
+ // ever runs, so there's always something to look at even right at
142
+ // the end of input. Added for fn.js's signature-line lookahead
143
+ // ("IDENT followed by '(' -- is this a signature, or the start of a
144
+ // statement?"), which expr()'s own single-token-lookahead grammar
145
+ // never needed.
146
+ peekNext() {
147
+ return this.tokens[this.i + 1];
148
+ }
149
+
139
150
  next() {
140
151
  return this.tokens[this.i++];
141
152
  }
package/fn.js CHANGED
@@ -10,7 +10,8 @@
10
10
  // statement-sequence wrapper around that, lowering to the real ast.js
11
11
  // builders (letChain, outputs), never a new node shape:
12
12
  //
13
- // program := stmt* returnStmt
13
+ // program := signature? stmt* returnStmt
14
+ // signature := IDENT "(" (IDENT ("," IDENT)*)? ")" ":"
14
15
  // stmt := "let" IDENT "=" expression ";"
15
16
  // returnStmt := "return" expression ";"
16
17
  // | "return" "{" IDENT ":" expression ("," IDENT ":" expression)* "}" ";"
@@ -24,7 +25,21 @@
24
25
  // Duplicate let-names are deliberately NOT checked here -- letChain()
25
26
  // doesn't check either (ast.js); collectLets() already does, at
26
27
  // emission time. Same "defer semantic validation to emission" precedent
27
- // expr.js itself follows for function/call names.
28
+ // expr.js itself follows for function/call names. Duplicate param names
29
+ // go unchecked for the same reason, and because nothing else in this
30
+ // project validates that today either -- {name, params, body} objects
31
+ // were always hand-built plain JS before this, with no infrastructure
32
+ // for it.
33
+ //
34
+ // The signature line is entirely optional, which makes fn's return type
35
+ // conditional on what you actually wrote: no signature -> a bare Node,
36
+ // exactly as before this was added (fully backward compatible -- every
37
+ // fn`...` template written before this feature existed still parses
38
+ // identically); a signature present -> the full {name, params, body}
39
+ // shape, usable directly in emitAll()/evaluate() with no wrapping. This
40
+ // was a deliberate API choice, not an oversight -- see the GitHub issue
41
+ // this shipped from for the alternative considered (a separate,
42
+ // always-full-definition tag) and why this was preferred.
28
43
  const { letChain, outputs } = require("./ast.js");
29
44
  const { Parser, tokenizeSegment } = require("./expr.js");
30
45
 
@@ -77,7 +92,42 @@ function parseReturnStatement(parser) {
77
92
  return node;
78
93
  }
79
94
 
95
+ // Signature lookahead needs 2 tokens, not 1: an IDENT that isn't "let"/
96
+ // "return" (those always start a statement instead), immediately
97
+ // followed by "(". That's a complete, unambiguous rule given the
98
+ // grammar above -- a fn`...` program only ever starts with a signature,
99
+ // a "let", or a "return", so there's no fourth case this could be
100
+ // confused with. (A malformed body missing its "let"/"return" entirely
101
+ // -- e.g. a bare `` fn`sqrt(x)` `` someone forgot the "return" on --
102
+ // still ends up an error either way, just reported as a missing ":"
103
+ // rather than a missing "return"; not worth deeper lookahead to improve
104
+ // one malformed-input error message.)
105
+ function looksLikeSignature(parser) {
106
+ const t = parser.peek();
107
+ if (t.type !== "IDENT" || t.value === "let" || t.value === "return") return false;
108
+ const next = parser.peekNext();
109
+ return next.type === "OP" && next.value === "(";
110
+ }
111
+
112
+ function parseSignature(parser) {
113
+ const name = expectIdent(parser, "as the function name starting a fn`...` signature");
114
+ parser.expectOp("(");
115
+ const params = [];
116
+ if (!parser.isOp(")")) {
117
+ params.push(expectIdent(parser, "as a parameter name in a fn`...` signature"));
118
+ while (parser.isOp(",")) {
119
+ parser.next();
120
+ params.push(expectIdent(parser, "as a parameter name in a fn`...` signature"));
121
+ }
122
+ }
123
+ parser.expectOp(")");
124
+ parser.expectOp(":");
125
+ return { name, params };
126
+ }
127
+
80
128
  function parseProgram(parser) {
129
+ const signature = looksLikeSignature(parser) ? parseSignature(parser) : null;
130
+
81
131
  const bindings = [];
82
132
  while (isKeyword(parser, "let")) {
83
133
  bindings.push(parseLetStatement(parser));
@@ -86,7 +136,9 @@ function parseProgram(parser) {
86
136
  parser.error('expected "return" (a fn`...` body is zero or more "let" statements followed by a "return")');
87
137
  }
88
138
  const body = parseReturnStatement(parser);
89
- return bindings.length > 0 ? letChain(bindings, body) : body;
139
+ const result = bindings.length > 0 ? letChain(bindings, body) : body;
140
+
141
+ return signature ? { name: signature.name, params: signature.params, body: result } : result;
90
142
  }
91
143
 
92
144
  // Same token-splicing loop expr() uses in expr.js -- see that file's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exprforge",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Author a math expression once as an AST (or readable infix text via expr/fn), emit identical-behavior implementations in JS, TypeScript, Python, C#, Lua, QB64, C, Java, Go, Rust, Perl, PHP, Julia, Fortran, Zig, Scheme, and COBOL, plus a native evaluator and its own readable syntax printer.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",