From 58f3ca50b0440ecd387f043116f0953ed940bee1 Mon Sep 17 00:00:00 2001 From: Torben Ewert Date: Sun, 31 May 2026 13:28:43 +0200 Subject: [PATCH] feat: implement compiler for javascript --- .github/workflows/publish-if-not-exists.js | 43 ++++++ .github/workflows/publish-npm.yml | 10 +- js/pincjsc/compile.ml | 62 ++++++++ js/pincjsc/dune | 17 +++ js/pincjsc/package-lock.json | 13 ++ js/pincjsc/package.json | 19 +++ js/pincjsc/pincjsc.d.ts | 1 + js/pincjsc/pincjsc.js | 9 ++ lib/00_core/StdlibExtension.ml | 30 ++++ lib/00_types/Ast.ml | 7 + lib/02_parsing/DependencyGraph.ml | 156 +++++++++++++++++++++ lib/02_parsing/DependencyGraph.mli | 10 ++ lib/02_parsing/Pinc_Parser.ml | 1 + lib/02_parsing/Pinc_Parser.mli | 1 + lib/Pinc_lang.ml | 1 + 15 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/publish-if-not-exists.js create mode 100644 js/pincjsc/compile.ml create mode 100644 js/pincjsc/dune create mode 100644 js/pincjsc/package-lock.json create mode 100644 js/pincjsc/package.json create mode 100644 js/pincjsc/pincjsc.d.ts create mode 100644 js/pincjsc/pincjsc.js create mode 100644 lib/02_parsing/DependencyGraph.ml create mode 100644 lib/02_parsing/DependencyGraph.mli diff --git a/.github/workflows/publish-if-not-exists.js b/.github/workflows/publish-if-not-exists.js new file mode 100644 index 0000000..c805512 --- /dev/null +++ b/.github/workflows/publish-if-not-exists.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); + +function getPackageJson(directory) { + const packageJson = path.resolve(directory, 'package.json'); + + if (fs.existsSync(packageJson)) { + return JSON.parse(fs.readFileSync(packageJson)); + } + + const parentDir = path.dirname(directory); + + if (parentDir === directory) { + // System root + throw new Error("Couldn't not find your package.json file"); + } + + return resolvePackage(path.dirname(directory)); +} + +const package = getPackageJson(process.cwd()); + +const tag = `${package.name}@${package.version}`; + +exec(`npm view ${tag} version`, (error, stdout, stderr) => { + if (stderr.includes('code E404') || (!error && stdout.trim() !== package.version.trim())) { + console.info(`${tag} does not exist. Publishing...`); + + const child = exec(`npm publish ${process.argv.slice(2).join(' ')}`); + child.stdout.pipe(process.stdout); + child.stderr.pipe(process.stderr); + child.on('exit', process.exit); + } else if (error) { + console.error('Something went wrong while checking the current version.'); + console.log(stdout); + console.error(stderr); + } else { + console.info('Already up-to-date.'); + } +}); diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 3e953a0..c4d53c7 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -94,12 +94,14 @@ jobs: name: js path: js - - name: Install NPM Dependencies + - name: Publish pincfmt run: | cd js/pincfmt npm ci + node ./.github/workflows/publish-if-not-exists.js - - name: Publish to Package Registry + - name: Publish pincjsc run: | - cd js/pincfmt - npm publish + cd js/pincjsc + npm ci + node ./.github/workflows/publish-if-not-exists.js diff --git a/js/pincjsc/compile.ml b/js/pincjsc/compile.ml new file mode 100644 index 0000000..94675bc --- /dev/null +++ b/js/pincjsc/compile.ml @@ -0,0 +1,62 @@ +open Js_of_ocaml +open Pinc_Core + +let get_files_with_ext ~ext dir = + let rec loop result = function + | file :: rest when Sys.is_directory file -> + Sys.readdir file + |> Array.to_list + |> List.map (Filename.concat file) + |> List.append rest + |> loop result + | file :: rest when Filename.extension file = ext -> loop (file :: result) rest + | _file :: rest -> loop result rest + | [] -> result + in + loop [] [ dir ] +;; + +let get_declarations_from ~directory () = + directory |> get_files_with_ext ~ext:".pi" |> List.map Pinc_Source.of_file +;; + +let compile input out = + let declarations = get_declarations_from ~directory:input () in + let ast = Pinc_Parser.get_ast declarations in + let dependency_graph = Pinc_Parser.DependencyGraph.build ast in + + ast + |> StringMap.iter (fun key declaration -> + let code = + declaration |> StringMap.singleton key |> Pinc_Types.Ast.marshal |> String.to_hex + in + let component_dependencies = + Pinc_Parser.DependencyGraph.dependencies_of dependency_graph key + |> StringSet.elements + in + + let imports = + component_dependencies + |> List.map (fun key -> + let path = Printf.sprintf "./%s.pi.mjs" key in + Printf.sprintf "import { __internal_code as %s } from %S;\n" key path) + |> String.concat "" + in + + let code_export = Printf.sprintf "export const __internal_code = %S;\n" code in + + let code = + component_dependencies + |> List.cons "__internal_code" + |> List.rev + |> String.concat "," + in + let export = Printf.sprintf "export default [%S,%s];" key code in + let result = imports ^ code_export ^ export in + + let filename = Printf.sprintf "%s.pi.mjs" key in + Out_channel.( + with_open_bin (Filename.concat out filename) (fun oc -> output_string oc result))) +;; + +let () = Js.export "compile" compile diff --git a/js/pincjsc/dune b/js/pincjsc/dune new file mode 100644 index 0000000..f22372b --- /dev/null +++ b/js/pincjsc/dune @@ -0,0 +1,17 @@ +(executable + (promote + (only compile.bc.js)) + (name compile) + (modules compile) + (modes js) + (libraries + pinc-lang.core + pinc-lang.source + pinc-lang.types + pinc-lang.parser + pinc-lang.backend + js_of_ocaml) + (js_of_ocaml + (compilation_mode whole_program) + (sourcemap no) + (flags --target-env=nodejs))) diff --git a/js/pincjsc/package-lock.json b/js/pincjsc/package-lock.json new file mode 100644 index 0000000..b19e76b --- /dev/null +++ b/js/pincjsc/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "@pinc-official/pincjs", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@pinc-official/pincjs", + "version": "0.0.1", + "license": "MIT" + } + } +} diff --git a/js/pincjsc/package.json b/js/pincjsc/package.json new file mode 100644 index 0000000..30c38d1 --- /dev/null +++ b/js/pincjsc/package.json @@ -0,0 +1,19 @@ +{ + "name": "@pinc-official/pincjsc", + "version": "0.0.1", + "description": "Provides functions to compile pinc-lang source code to be executable in JavaScript", + "homepage": "https://github.com/pinc-official/pinc-lang#readme", + "bugs": { + "url": "https://github.com/pinc-official/pinc-lang/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/pinc-official/pinc-lang.git" + }, + "license": "MIT", + "author": "Torben Ewert ", + "type": "commonjs", + "main": "./pincjsc.js", + "types": "./pincjsc.d.ts", + "dependencies": {} +} diff --git a/js/pincjsc/pincjsc.d.ts b/js/pincjsc/pincjsc.d.ts new file mode 100644 index 0000000..f21db5a --- /dev/null +++ b/js/pincjsc/pincjsc.d.ts @@ -0,0 +1 @@ +export function compile(inputDir: string, outDir: string): void; diff --git a/js/pincjsc/pincjsc.js b/js/pincjsc/pincjsc.js new file mode 100644 index 0000000..b6a14c3 --- /dev/null +++ b/js/pincjsc/pincjsc.js @@ -0,0 +1,9 @@ +const { compile } = require('./compile.bc'); + +const compile_wrapper = (inputDir, outDir) => { + compile(inputDir, outDir); +}; + +module.exports = { + compile: compile_wrapper, +}; diff --git a/lib/00_core/StdlibExtension.ml b/lib/00_core/StdlibExtension.ml index 38ff91c..6c33a8b 100644 --- a/lib/00_core/StdlibExtension.ml +++ b/lib/00_core/StdlibExtension.ml @@ -145,4 +145,34 @@ module String = struct else empty ;; + + let to_hex d = + let char_hex n = + Char.chr + (if n < 10 then + Char.code '0' + n + else + Char.code 'a' + n - 10) + in + let len = String.length d in + let result = Bytes.create (len * 2) in + for i = 0 to len - 1 do + let x = Char.code d.[i] in + Bytes.unsafe_set result (i * 2) (char_hex (x lsr 4)); + Bytes.unsafe_set result ((i * 2) + 1) (char_hex (x land 0x0f)) + done; + Bytes.unsafe_to_string result + ;; + + let of_hex s = + let digit c = + match c with + | '0' .. '9' -> Char.code c - Char.code '0' + | 'A' .. 'F' -> Char.code c - Char.code 'A' + 10 + | 'a' .. 'f' -> Char.code c - Char.code 'a' + 10 + | _ -> invalid_arg "String.of_hex" + in + let byte i = (digit s.[i] lsl 4) + digit s.[i + 1] in + String.init (String.length s / 2) (fun i -> Char.chr (byte (2 * i))) + ;; end diff --git a/lib/00_types/Ast.ml b/lib/00_types/Ast.ml index 3e2bad0..24c9dfe 100644 --- a/lib/00_types/Ast.ml +++ b/lib/00_types/Ast.ml @@ -139,6 +139,13 @@ and declaration_kind = and t = declaration StringMap.t +let marshal (d : t) = Marshal.to_string d [] + +let unmarshal s = + let result : t = Marshal.from_string s 0 in + result +;; + module Declaration = struct let marshal (d : declaration) = Marshal.to_string d [] diff --git a/lib/02_parsing/DependencyGraph.ml b/lib/02_parsing/DependencyGraph.ml new file mode 100644 index 0000000..f4d670b --- /dev/null +++ b/lib/02_parsing/DependencyGraph.ml @@ -0,0 +1,156 @@ +type t = StringSet.t StringMap.t + +let rec collect_expr acc (expr : Pinc_Types.Ast.expression) = + match expr.expression_desc with + | Void + | String _ + | Char _ + | Int _ + | Float _ + | Bool _ + | LowercaseIdentifierExpression _ + | ExternalFunction _ -> acc + | UppercaseIdentifierExpression name -> StringSet.add name acc + | Array elems -> Array.fold_left collect_expr acc elems + | Record fields -> StringMap.fold (fun _ (_, e) a -> collect_expr a e) fields acc + | Function { body; _ } -> collect_expr acc body + | FunctionCall { function_definition; arguments } -> + let acc = collect_expr acc function_definition in + List.fold_left collect_expr acc arguments + | TagExpression tag -> collect_tag acc tag + | ForInExpression { iterable; body; _ } -> + let acc = collect_expr acc iterable in + collect_expr acc body + | TemplateExpression node -> collect_template_node acc node + | BlockExpression stmts -> List.fold_left collect_stmt acc stmts + | ConditionalExpression { condition; consequent; alternate } -> ( + let acc = collect_expr acc condition in + let acc = collect_expr acc consequent in + match alternate with + | None -> acc + | Some s -> collect_expr acc s) + | UnaryExpression (_, e) -> collect_expr acc e + | BinaryExpression (l, _, r) -> + let acc = collect_expr acc l in + collect_expr acc r + +and collect_stmt acc (stmt : Pinc_Types.Ast.statement) = + match stmt.statement_desc with + | BreakStatement _ | ContinueStatement _ -> acc + | LetStatement (_, e) + | MutableLetStatement (_, e) + | OptionalLetStatement (_, e) + | OptionalMutableLetStatement (_, e) + | MutationStatement (_, e) + | ExpressionStatement e -> collect_expr acc e + +and collect_tag acc (tag : Pinc_Types.Ast.tag) = + let acc = StringMap.fold (fun _ e a -> collect_expr a e) tag.tag_desc.attributes acc in + let acc = + match tag.tag_desc.transformer with + | None -> acc + | Some e -> collect_expr acc e + in + match tag.tag_desc.children with + | None -> acc + | Some e -> collect_expr acc e + +and collect_template_node acc (node : Pinc_Types.Ast.template_node) = + match node.template_node_desc with + | TextTemplateNode _ -> acc + | FragmentTemplateNode fragment_children -> + List.fold_left collect_template_node acc fragment_children + | ExpressionTemplateNode e -> collect_expr acc e + | HtmlTemplateNode { html_tag_attributes; html_tag_children; _ } -> + let acc = StringMap.fold (fun _ e a -> collect_expr a e) html_tag_attributes acc in + List.fold_left collect_template_node acc html_tag_children + | ComponentTemplateNode + { + component_tag_identifier = Uppercase_Id (name, _); + component_tag_attributes; + component_tag_children; + } -> + let acc = StringSet.add name acc in + let acc = + StringMap.fold (fun _ e a -> collect_expr a e) component_tag_attributes acc + in + List.fold_left collect_template_node acc component_tag_children +;; + +let declaration_deps (decl : Pinc_Types.Ast.declaration) : StringSet.t = + let acc = + StringMap.fold + (fun _ e a -> collect_expr a e) + decl.declaration_attributes + StringSet.empty + in + collect_expr acc decl.declaration_body +;; + +let cyclic_dependencies (graph : t) = + let resolve_status = Hashtbl.create (StringMap.cardinal graph) in + + let rec aux key dependencies resolved_as_circular path = + Hashtbl.replace resolve_status key `Unresolved; + + let result = + StringSet.fold + (fun dependency resolved_as_circular -> + let path = dependency :: path in + match Hashtbl.find_opt resolve_status dependency with + | Some `Resolved -> resolved_as_circular + | Some `Unresolved -> (key, List.rev path) :: resolved_as_circular + | None -> ( + match StringMap.find_opt dependency graph with + | None -> resolved_as_circular + | Some dependencies -> aux dependency dependencies resolved_as_circular path + )) + dependencies + resolved_as_circular + in + + Hashtbl.replace resolve_status key `Resolved; + result + in + + StringMap.fold (fun key dependencies acc -> aux key dependencies acc [ key ]) graph [] +;; + +let report_cyclic_dependencies ast deps = + List.iter + (fun (key, path) -> + let declaration = StringMap.find key ast in + let path = String.concat " -> " path in + Pinc_Diagnostics.raise_error + declaration.Pinc_Types.Ast.declaration_loc + (Printf.sprintf "Found cyclic dependency in `%s`:\n%s\n%!" key path)) + deps +;; + +let build (ast : Pinc_Types.Ast.t) : t = + let graph = StringMap.map declaration_deps ast in + let () = report_cyclic_dependencies ast @@ cyclic_dependencies graph in + graph +;; + +let dependencies_of (graph : t) (name : string) : StringSet.t = + StringMap.find_opt name graph |> Option.value ~default:StringSet.empty +;; + +let transitive_dependencies_of (graph : t) (name : string) : StringSet.t = + let rec aux acc name = + let direct = dependencies_of graph name in + + StringSet.fold + (fun dep acc -> + if StringSet.mem dep acc then + acc + else ( + let acc = StringSet.add dep acc in + StringSet.union acc (aux acc dep))) + direct + acc + in + + aux StringSet.empty name +;; diff --git a/lib/02_parsing/DependencyGraph.mli b/lib/02_parsing/DependencyGraph.mli new file mode 100644 index 0000000..dd3aa98 --- /dev/null +++ b/lib/02_parsing/DependencyGraph.mli @@ -0,0 +1,10 @@ +type t + +(** Build a dependency graph from a parsed AST. *) +val build : Pinc_Types.Ast.t -> t + +(** Direct dependencies of a declaration. *) +val dependencies_of : t -> string -> StringSet.t + +(** All declarations needed to compile a given declaration. *) +val transitive_dependencies_of : t -> string -> StringSet.t diff --git a/lib/02_parsing/Pinc_Parser.ml b/lib/02_parsing/Pinc_Parser.ml index 43f15fa..dc4a7cd 100644 --- a/lib/02_parsing/Pinc_Parser.ml +++ b/lib/02_parsing/Pinc_Parser.ml @@ -1,3 +1,4 @@ +module DependencyGraph = DependencyGraph module Parsetree = Parsetree module Transformer = Transformer diff --git a/lib/02_parsing/Pinc_Parser.mli b/lib/02_parsing/Pinc_Parser.mli index 9882a64..4e24537 100644 --- a/lib/02_parsing/Pinc_Parser.mli +++ b/lib/02_parsing/Pinc_Parser.mli @@ -1,3 +1,4 @@ +module DependencyGraph = DependencyGraph module Parsetree = Parsetree module Transformer = Transformer diff --git a/lib/Pinc_lang.ml b/lib/Pinc_lang.ml index 17c96f5..5241355 100644 --- a/lib/Pinc_lang.ml +++ b/lib/Pinc_lang.ml @@ -10,4 +10,5 @@ module Interpreter = struct end module StringMap = Pinc_Core.StringMap +module StringSet = Pinc_Core.StringSet module Helpers = Pinc_Backend.Helpers -- 2.51.2