npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

arith2js

v1.0.6

Published

* See section [Análisis Sintáctico Ascendente en JavaScript](http://crguezl.github.io/pl-html/node43.html) * The example working at [http://crguezl.github.io/jison-minus/](http://crguezl.github.io/jison-minus/)

Readme

Open in Codespaces

Práctica: Traducción de expresiones aritméticas a JavaScript

Introducción

Enunciado de la práctica arith2js

En la presente práctica se pretende implementar un traductor de expresiones aritméticas a JavaScript. Para ello se utilizará la herramienta Jison que es un generador de analizadores sintácticos (parsers) a partir de una gramática y un analizador léxico.

Primeros pasos

Instalar Dependencias

npm install

Código

Crear el fichero de gramática

%{
const { 
 L,
 buildLiteral, 
 buildBinaryExpression, 
 buildRoot,
 buildUnaryExpression,
 buildCallExpression
} = require('./ast-build.js');
%}
%token 
%left '+' '-'
%left '*' '/'
%right '**'
%left '!'
%nonassoc UMINUS
%%
es: e EOF { return buildRoot($1, L(@1, yy)); }
;

e:
   e '-' e     { $$ = buildBinaryExpression($1, $2, $3, L(@2, yy)); }
 | e '+' e     { $$ = buildBinaryExpression($1, $2, $3, L(@2, yy)); }
 | e '*' e     { $$ = buildBinaryExpression($1, $2, $3, L(@2, yy)); }
 | e '/' e     { $$ = buildBinaryExpression($1, $2, $3, L(@2, yy)); }
 | '(' e ')'   { $$ = $2; }
 | e '**' e    { $$ = buildCallExpression('power', [$1, $3], L(@2, yy)); }
 | e '!'       { $$ = buildCallExpression('factorial', [$1], L(@2, yy)); }
 | '-' e %prec UMINUS { $$ = buildUnaryExpression($1, $2, L(@1, yy)); }
 | N           { $$ = buildLiteral($1, L(@1, yy)); }
;

Crear el fichero de construcción del AST en el que se encuentran las funciones que construyen los nodos del árbol sintáctico abstracto

function buildRoot(child, loc) {
  return {
    "type": "Program",
    "start": [loc.start.line - 1] + loc.start.column,
    "end": [loc.end.line - 1] + loc.end.column,
    "loc": loc,
    "range": [
      [loc.start.line - 1] + loc.start.column,
      [loc.end.line - 1] + loc.end.column
    ],
    "body": [
      {
        "type": "ExpressionStatement",
        "start": [loc.start.line - 1] + loc.start.column,
        "end": [loc.end.line - 1] + loc.end.column,
        "loc": loc,
        "range": [
          [loc.start.line - 1] + loc.start.column,
          [loc.end.line - 1] + loc.end.column
        ],
        "expression": child
      }
    ],
    "sourceType": "script"
  };
}

function L(jloc, { input, offsets }) {
  return {
    "start": {
      "line": jloc.first_line,
      "column": jloc.first_column
    },
    "end": {
      "line": jloc.last_line,
      "column": jloc.last_column
    }
  };
}

function computeLineOffets(input) {
  let offsets = [];
  let lines = input.split("\n");
  let offset = 0;
  for (let i = 0; i < lines.length; i++) {
    offsets.push(offset);
    offset += lines[i].length + 1;
  }
  return offsets;
}

function buildLiteral(raw, loc) {
  return {
    "type": "Literal",
    "start": [loc.start.line - 1] + loc.start.column,
    "end":  [loc.end.line - 1] + loc.end.column,
    "loc": loc,
    "range": [
      [loc.start.line - 1] + loc.start.column,
      [loc.end.line - 1] + loc.end.column
    ],
    "value": Number(raw),
    "raw": raw
  };
}

function buildBinaryExpression(left, op, right, loc) {
  return {
    "type": "BinaryExpression",
    "start": [loc.start.line - 1] + loc.start.column,
    "end": [loc.end.line - 1] + loc.end.column,
    "loc": loc,
    "range": [
      [loc.start.line - 1] + loc.start.column,
      [loc.end.line - 1] + loc.end.column
    ],
    "operator": op,
    "left": left,
    "right": right
  };
}

function buildUnaryExpression(op, argument, loc) {
  return {
    "type": "UnaryExpression",
    "start": [loc.start.line - 1] + loc.start.column,
    "end": [loc.end.line - 1] + loc.end.column,
    "loc": loc,
    "range": [
      [loc.start.line - 1] + loc.start.column,
      [loc.end.line - 1] + loc.end.column
    ],
    "operator": op,
    "prefix": true,
    "argument": argument
  };
}

function buildCallExpression(callee, args, loc) {
  return {
    "type": "CallExpression",
    "start": [loc.start.line - 1] + loc.start.column,
    "end": [loc.end.line - 1] + loc.end.column,
    "loc": loc,
    "range": [
      [loc.start.line - 1] + loc.start.column,
      [loc.end.line - 1] + loc.end.column
    ],
    "callee": {
      "type": "Identifier",
      "start": [loc.start.line - 1] + loc.start.column,
      "end": [loc.end.line - 1] + loc.end.column,
      "loc": loc,
      "range": [
        [loc.start.line - 1] + loc.start.column,
        [loc.end.line - 1] + loc.end.column
      ],
      "name": callee
    },
    "arguments": args
  };
}

module.exports = {
    buildRoot,
    buildBinaryExpression,
    buildUnaryExpression,
    buildLiteral,
    computeLineOffets,
    L,
    buildCallExpression
};

Crear el fichero con la función transpile que se encargará de generar el código JavaScript a partir del AST

#!/usr/bin/env node
const fs = require('fs/promises');

const p = require("./calc").parser;

const estraverse = require("estraverse"); 
const exportedSupportIdentifiers = Object.keys(require("./support-lib.js")); // [ power, factorial ]

const escodegen = require('escodegen');
const { renderFile } = require('template-file'); // handlebars.js mustache.js ...

const computeLineOffets = require('./ast-build').computeLineOffets;

async function generateCode(dependencies, ast) {
  let code = escodegen.generate(ast);
  code = `console.log(${code.slice(0, -1)});`;
  if (dependencies.length === 0) {
    return code;
  }
  let root = __dirname.replace(/\/src$/, '');
  return renderFile(`${root}/src/template.js`, { root, code, dependencies });
}

const findUsedFunctions = function (ast) {
  const usedSupportFunctions = new Set();
  estraverse.traverse(ast, {
    enter: function (node, _ ) {
      if (node.type === "CallExpression" && node.callee.type === "Identifier" && exportedSupportIdentifiers.includes(node.callee.name)) {
        usedSupportFunctions.add(node.callee.name);
      }
    },
  });
  return Array.from(usedSupportFunctions);
}

async function writeOutput(outputFile, output, options) {
  let finalOutput = output;
  if (options?.verbose) {
    console.log(finalOutput);
  }
  if (!outputFile) {
    return finalOutput;
  }
  await fs.writeFile(outputFile, finalOutput);
  return finalOutput;
}

module.exports = async function transpile(inputFile, options) {
  let outputFile = options?.output || "./out/output.js";
  try {
    let input = await fs.readFile(inputFile, 'utf-8')
    let offsets = computeLineOffets(input);           

    p.yy = { input, offsets };      
    const ast = p.parse(input);

    let dependencies = findUsedFunctions(ast);        

    const output = await generateCode(dependencies, ast);
    
    return await writeOutput(outputFile, output, options);
  } catch (e) {
    console.error(e.message);
  }
};

Crear el fichero de ejecución bin/calc2js.mjs

#!/usr/bin/env node

import { program } from "commander";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { version } = require("../package.json");
import transpile from "../src/transpile.js";
import run  from "../src/run.js";


program
  .version(version)
  .showSuggestionAfterError(true);

program
  .command("transpile")
  .argument("<filename>", "calc file to transpile")
  .option("-o, --output <filename>", "output file")
  .option("-v, --verbose", "show generated code")
  .description("Transpile a calc file to JavaScript")
  .action((filename, options) => {
    transpile(filename, options); // Options es: { output: "archivosalida", verbose: true/false }
  });
  
program.parse(process.argv);

Ejecutar el programa

Para ejecutar el programa se debe ejecutar el siguiente comando que transpilará el archivo test/test1.calc a JavaScript

node bin/calc2js.mjs transpile test/test1.calc -o out.js

Si queremos que se ejecute el código generado, podemos usar la opción -v para que se muestre el código y pasarlo por pipeline a node

node bin/calc2js.mjs transpile test/test1.calc -o out.js -v | node

Adiciones

Con lo anterior tendríamos suficiente para ejecutar algunos archivos con operaciones básicas, pero necesitamos importar algunas funciones que no están definidas en el código que hemos escrito. Para ello, se ha creado un fichero support-lib.js que contiene las funciones power y factorial.

Crear el fichero support-lib.js

let gamma = require('math-gamma');
const power = (base, exponent) => Math.pow(base, exponent);
const factorial = (number) => gamma(number + 1);

module.exports = { 
  power, 
  factorial 
};

Crear el fichero template.js

Se usará template-file para añadir las dependencias solo cuando se necesiten

const { {{ dependencies }} } = require('{{root}}/src/support-lib.js');
{{code}}

Función run

Es incómodo tener que ejecutar el código generado con node, por lo que se ha creado una función run que se encarga de ejecutar el código generado

const transpile = require("./transpile.js");

module.exports = async function run(inputFile, options) {
  const code = await transpile(inputFile, options);
  eval(code);
};

Ahora se puede ejecutar con el siguiente comando

node bin/calc2js.mjs run test/test1.calc

Añadiendo la opción -v se mostrará el código generado

node bin/calc2js.mjs run test/test1.calc -v

Testing con Mocha

Se ha creado un fichero test.js en el que se automatiza la ejecución de los tests

const transpile = require("../src/transpile.js");
const assert = require('assert');
const fs = require("fs/promises");

require('dotenv').config();
const JSComparison = process.env.JSComparison === 'true';
const REMOVE_OUTPUTS = process.env.REMOVE_OUTPUTS === 'true';

const Tst = require('./test-description.js');

const Test = Tst.map(t => ({
  input: __dirname + '/data/' + t.input,
  output: __dirname + '/data/' + t.output,
  expected: __dirname + '/data/' + t.expected,
  correctOut: __dirname + '/data/' + t.correctOut,
})
)

const removeRequires = /require\(["'][\S]+["']\)/g;

function removeSpaces(s) {
  return String(s).replace(/\s/g, '').replace(removeRequires, 'require("support-lib")');
}

async function outputProgramIsAsExpected(t, outputjs) {
  let expectedjs = await fs.readFileSync(t.expectedjs, 'utf-8')
  
  assert.equal(removeSpaces(outputjs), removeSpaces(expectedjs));
  if (REMOVE_OUTPUTS) fs.unlinkSync(t.output);
  return outputjs;
}

async function outputRunExpected(outputjs, expectedout) {
  expectedout = await fs.readFile(expectedout, 'utf8');
  oldLog = console.log;
  let result = '';
  console.log = (...s) => result += s.join(' ');
  eval(outputjs);
  assert.equal(removeSpaces(result), removeSpaces(expectedout));
  console.log = oldLog;
  return result;
}

async function main() {
  for (let i = 0; i < Test.length; i++) {
    let [t, ft] = [Test[i], Test[i].input];
    it(`Test ${i + 1}: ${ft}`, async () => {
      let outputjs = await transpile(ft, t.output);
      if (JSComparison) await outputProgramIsAsExpected(t, outputjs);
      await outputRunExpected(outputjs, t.correctOut);
    });
  }
}

main();

Para ejecutar los tests se debe ejecutar el siguiente script

npm run test
npm test

Se pueden añadir más tests en el fichero test-description.js y creando su entrada su código js esperado y su salida esperada en archivos dentro del directorio test/data/ del repo

Conclusiones

Con la implementación de la práctica se ha conseguido traducir expresiones aritméticas a JavaScript. Se ha utilizado Jison para generar el parser y se ha creado un AST con las funciones necesarias para construirlo. Se ha añadido soporte para funciones power y factorial que no están definidas en el código. Se ha creado un fichero template.js para añadir las dependencias solo cuando se necesiten. Se ha creado una función run que se encarga de ejecutar el código generado. Se ha automatizado la ejecución de los tests con Mocha.

Yo haciendo la práctica

Referencias