Skip to content

Commit 8fc837f

Browse files
authored
Add app metadata introspection APIs (#703)
* Add app metadata system APIs * fix(#673): bake metadata in JS emit, dedupe perry.toml parse, regen API docs Address review feedback on PR #703: - JS emit path (`crates/perry-codegen-js`) now intercepts `getAppVersion` / `getAppBuildNumber` / `getBundleId` and emits string/number literals from AppMetadata, instead of falling through to `console.warn('… not available in browser')`. Web/JS builds now resolve the same values as native. - `read_app_metadata` takes `Option<&toml::Table>` instead of `Option<&Path>`, so `perry.toml` is parsed exactly once in `run_with_parse_cache` and shared with the existing i18n block. - `CompilationContext.app_metadata` exposes the resolved metadata to all codegen backends — used by the web/JS target wiring above and available for future arkts / wasm consumers. - Tests: add the missing `version` assertion in `cli_bundle_id_overrides_…` and a new `package_json_bundle_id_falls_back_…` case covering the package.json fallback branch. - Regenerate `docs/api/perry.d.ts` and `docs/src/api/reference.md` — fixes the `api-docs-drift` CI gate that was failing on this PR.
1 parent 72e695c commit 8fc837f

17 files changed

Lines changed: 478 additions & 85 deletions

File tree

crates/perry-api-manifest/src/entries.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,6 +1845,9 @@ pub static API_MANIFEST: &[ApiEntry] = &[
18451845
method("perry/system", "getDeviceIdiom", false, None),
18461846
method("perry/system", "getDeviceModel", false, None),
18471847
method("perry/system", "getLocale", false, None),
1848+
method("perry/system", "getAppVersion", false, None),
1849+
method("perry/system", "getAppBuildNumber", false, None),
1850+
method("perry/system", "getBundleId", false, None),
18481851
method("perry/system", "getAppIcon", false, None),
18491852
method("perry/system", "openURL", false, None),
18501853
method("perry/system", "keychainSave", false, None),

crates/perry-codegen-js/src/emit.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ use perry_types::{FuncId, GlobalId, LocalId};
77
use std::collections::{BTreeMap, BTreeSet};
88
use std::fmt::Write as FmtWrite;
99

10+
/// App metadata baked into compile-time `perry/system` introspection APIs
11+
/// (`getAppVersion`/`getAppBuildNumber`/`getBundleId`) when emitting JS.
12+
///
13+
/// Mirrors `perry_codegen::AppMetadata` — duplicated here so this crate
14+
/// doesn't take a backend dep on perry-codegen.
15+
#[derive(Debug, Clone)]
16+
pub struct AppMetadata {
17+
pub version: String,
18+
pub build_number: i64,
19+
pub bundle_id: String,
20+
}
21+
22+
impl Default for AppMetadata {
23+
fn default() -> Self {
24+
Self {
25+
version: "1.0.0".to_string(),
26+
build_number: 1,
27+
bundle_id: "com.perry.app".to_string(),
28+
}
29+
}
30+
}
31+
1032
/// JavaScript code emitter that translates HIR to JavaScript.
1133
pub struct JsEmitter {
1234
/// Output buffer
@@ -29,10 +51,16 @@ pub struct JsEmitter {
2951
minify: bool,
3052
/// Counter for generating short mangled names
3153
mangle_counter: usize,
54+
/// App metadata baked into compile-time `perry/system` introspection APIs.
55+
app_metadata: AppMetadata,
3256
}
3357

3458
impl JsEmitter {
3559
pub fn new(module_name: &str, minify: bool) -> Self {
60+
Self::with_metadata(module_name, minify, AppMetadata::default())
61+
}
62+
63+
pub fn with_metadata(module_name: &str, minify: bool, app_metadata: AppMetadata) -> Self {
3664
Self {
3765
output: String::with_capacity(8192),
3866
indent: 0,
@@ -44,6 +72,7 @@ impl JsEmitter {
4472
exported_names: BTreeSet::new(),
4573
minify,
4674
mangle_counter: 0,
75+
app_metadata,
4776
}
4877
}
4978

@@ -3160,6 +3189,17 @@ impl JsEmitter {
31603189
"getDeviceModel" | "get_device_model" => {
31613190
self.output.push_str("perry_system_get_device_model()");
31623191
}
3192+
"getAppVersion" | "get_app_version" => {
3193+
let s = self.quote_string(&self.app_metadata.version.clone());
3194+
self.output.push_str(&s);
3195+
}
3196+
"getAppBuildNumber" | "get_app_build_number" => {
3197+
let _ = write!(self.output, "{}", self.app_metadata.build_number);
3198+
}
3199+
"getBundleId" | "get_bundle_id" => {
3200+
let s = self.quote_string(&self.app_metadata.bundle_id.clone());
3201+
self.output.push_str(&s);
3202+
}
31633203
_ => {
31643204
let _ = write!(
31653205
self.output,

crates/perry-codegen-js/src/lib.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
pub mod emit;
77
pub mod minify;
88

9+
pub use emit::AppMetadata;
10+
911
use anyhow::Result;
1012
use perry_hir::ir::{Module, Stmt};
1113
use std::collections::BTreeSet;
@@ -16,7 +18,17 @@ const WEB_RUNTIME_JS: &str = include_str!("web_runtime.js");
1618
/// Compile a single HIR module to JavaScript source code.
1719
/// Returns (js_source, exported_names).
1820
pub fn compile_module_to_js(module: &Module, minify_names: bool) -> (String, BTreeSet<String>) {
19-
let emitter = emit::JsEmitter::new(&module.name, minify_names);
21+
compile_module_to_js_with_metadata(module, minify_names, AppMetadata::default())
22+
}
23+
24+
/// Compile a single HIR module to JavaScript source code, baking the supplied
25+
/// app metadata into compile-time `perry/system` introspection APIs.
26+
pub fn compile_module_to_js_with_metadata(
27+
module: &Module,
28+
minify_names: bool,
29+
app_metadata: AppMetadata,
30+
) -> (String, BTreeSet<String>) {
31+
let emitter = emit::JsEmitter::with_metadata(&module.name, minify_names, app_metadata);
2032

2133
// Collect names that have runtime values (not type-only exports)
2234
let mut runtime_names = BTreeSet::new();
@@ -61,6 +73,17 @@ pub fn compile_modules_to_html(
6173
modules: &[(String, Module)], // (module_name, hir_module)
6274
title: &str,
6375
minify: bool,
76+
) -> Result<String> {
77+
compile_modules_to_html_with_metadata(modules, title, minify, AppMetadata::default())
78+
}
79+
80+
/// Same as `compile_modules_to_html` but bakes the supplied app metadata into
81+
/// compile-time `perry/system` introspection APIs.
82+
pub fn compile_modules_to_html_with_metadata(
83+
modules: &[(String, Module)],
84+
title: &str,
85+
minify: bool,
86+
app_metadata: AppMetadata,
6487
) -> Result<String> {
6588
let mut all_js = String::with_capacity(32768);
6689
let mut declared_names = BTreeSet::new();
@@ -71,7 +94,8 @@ pub fn compile_modules_to_html(
7194
for (i, (mod_name, module)) in modules.iter().enumerate() {
7295
let is_entry = i == entry_idx;
7396

74-
let (js, exported_names) = compile_module_to_js(module, minify);
97+
let (js, exported_names) =
98+
compile_module_to_js_with_metadata(module, minify, app_metadata.clone());
7599

76100
if is_entry {
77101
// Entry module: emit directly (no IIFE wrapper)

crates/perry-codegen/src/codegen.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ use crate::stmt;
4141
use crate::strings::StringPool;
4242
use crate::types::{LlvmType, DOUBLE, I32, I64, I8, PTR, VOID};
4343

44+
/// Per-application metadata read from `perry.toml` by the CLI and baked into
45+
/// compile-time system APIs.
46+
#[derive(Debug, Clone, PartialEq, Eq)]
47+
pub struct AppMetadata {
48+
pub version: String,
49+
pub build_number: i64,
50+
pub bundle_id: String,
51+
}
52+
53+
impl Default for AppMetadata {
54+
fn default() -> Self {
55+
Self {
56+
version: "1.0.0".to_string(),
57+
build_number: 1,
58+
bundle_id: "com.perry.app".to_string(),
59+
}
60+
}
61+
}
62+
4463
/// Options controlling code generation for a single module.
4564
#[derive(Debug, Clone, Default)]
4665
pub struct CompileOptions {
@@ -186,6 +205,8 @@ pub struct CompileOptions {
186205
/// Drives `crate::block::FAST_MATH`; included in the object cache
187206
/// key so toggling it invalidates cached `.o` bytes.
188207
pub fast_math: bool,
208+
/// App metadata backing `perry/system` compile-time introspection APIs.
209+
pub app_metadata: AppMetadata,
189210
}
190211

191212
/// A class imported from another native module.
@@ -342,6 +363,8 @@ pub(crate) struct CrossModuleCtx {
342363
/// dead branches (which may reference FFI functions that don't exist on
343364
/// the current target).
344365
pub compile_time_constants: std::collections::HashMap<u32, f64>,
366+
/// App metadata backing compile-time `perry/system` introspection APIs.
367+
pub app_metadata: AppMetadata,
345368
/// Functions with a 3-param clamp pattern: fid → true. Call sites
346369
/// emit `@llvm.smax.i32` + `@llvm.smin.i32` instead of a function call.
347370
pub clamp3_functions: std::collections::HashSet<u32>,
@@ -1088,6 +1111,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
10881111
geisterhand_port: opts.geisterhand_port,
10891112
needs_js_runtime: opts.needs_js_runtime,
10901113
compile_time_constants,
1114+
app_metadata: opts.app_metadata.clone(),
10911115
clamp3_functions: hir
10921116
.functions
10931117
.iter()
@@ -2928,6 +2952,7 @@ fn compile_function(
29282952
local_id_to_name: HashMap::new(),
29292953
imported_vars: &cross_module.imported_vars,
29302954
compile_time_constants: &cross_module.compile_time_constants,
2955+
app_metadata: &cross_module.app_metadata,
29312956
scalar_replaced: std::collections::HashMap::new(),
29322957
scalar_replaced_arrays: std::collections::HashMap::new(),
29332958
scalar_ctor_target: Vec::new(),
@@ -3306,6 +3331,7 @@ fn compile_closure(
33063331
local_id_to_name: HashMap::new(),
33073332
imported_vars: &cross_module.imported_vars,
33083333
compile_time_constants: &cross_module.compile_time_constants,
3334+
app_metadata: &cross_module.app_metadata,
33093335
scalar_replaced: std::collections::HashMap::new(),
33103336
scalar_replaced_arrays: std::collections::HashMap::new(),
33113337
scalar_ctor_target: Vec::new(),
@@ -3529,6 +3555,7 @@ fn compile_method(
35293555
local_id_to_name: HashMap::new(),
35303556
imported_vars: &cross_module.imported_vars,
35313557
compile_time_constants: &cross_module.compile_time_constants,
3558+
app_metadata: &cross_module.app_metadata,
35323559
scalar_replaced: std::collections::HashMap::new(),
35333560
scalar_replaced_arrays: std::collections::HashMap::new(),
35343561
scalar_ctor_target: Vec::new(),
@@ -3959,6 +3986,7 @@ fn compile_module_entry(
39593986
local_id_to_name: HashMap::new(),
39603987
imported_vars: &cross_module.imported_vars,
39613988
compile_time_constants: &cross_module.compile_time_constants,
3989+
app_metadata: &cross_module.app_metadata,
39623990
scalar_replaced: std::collections::HashMap::new(),
39633991
scalar_replaced_arrays: std::collections::HashMap::new(),
39643992
scalar_ctor_target: Vec::new(),
@@ -4219,6 +4247,7 @@ fn compile_module_entry(
42194247
local_id_to_name: HashMap::new(),
42204248
imported_vars: &cross_module.imported_vars,
42214249
compile_time_constants: &cross_module.compile_time_constants,
4250+
app_metadata: &cross_module.app_metadata,
42224251
scalar_replaced: std::collections::HashMap::new(),
42234252
scalar_replaced_arrays: std::collections::HashMap::new(),
42244253
scalar_ctor_target: Vec::new(),
@@ -4928,6 +4957,7 @@ fn compile_static_method(
49284957
local_id_to_name: HashMap::new(),
49294958
imported_vars: &cross_module.imported_vars,
49304959
compile_time_constants: &cross_module.compile_time_constants,
4960+
app_metadata: &cross_module.app_metadata,
49314961
scalar_replaced: std::collections::HashMap::new(),
49324962
scalar_replaced_arrays: std::collections::HashMap::new(),
49334963
scalar_ctor_target: Vec::new(),

crates/perry-codegen/src/expr.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use perry_hir::{BinaryOp, CompareOp, Expr, UnaryOp, UpdateOp};
1313
use perry_types::Type as HirType;
1414

1515
use crate::block::LlBlock;
16+
use crate::codegen::AppMetadata;
1617
use crate::function::LlFunction;
1718
use crate::lower_call::{lower_call, lower_native_method_call, lower_new};
1819
use crate::lower_conditional::{lower_conditional, lower_logical, lower_truthy};
@@ -556,6 +557,8 @@ pub(crate) struct FnCtx<'a> {
556557
/// may reference extern FFI functions that don't exist on the current
557558
/// target (e.g., iOS-only `hone_get_documents_dir` on macOS).
558559
pub compile_time_constants: &'a std::collections::HashMap<u32, f64>,
560+
/// App metadata backing compile-time `perry/system` introspection APIs.
561+
pub app_metadata: &'a AppMetadata,
559562

560563
/// Scalar-replaced non-escaping objects. When `let p = new Point(x, y)`
561564
/// and `p` never escapes, instead of heap-allocating, each field gets a

crates/perry-codegen/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ pub mod stubs;
2525
pub(crate) mod type_analysis;
2626
pub mod types;
2727

28-
pub use codegen::{compile_module, resolve_target_triple, CompileOptions, ImportedClass};
28+
pub use codegen::{
29+
compile_module, resolve_target_triple, AppMetadata, CompileOptions, ImportedClass,
30+
};
2931

3032
/// One row of the native-module dispatch table, projected to just
3133
/// the manifest-relevant fields (module / method / has_receiver /

crates/perry-codegen/src/lower_call.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,21 @@ pub(crate) fn lower_call(ctx: &mut FnCtx<'_>, callee: &Expr, args: &[Expr]) -> R
817817
ctx.block().call_void("js_gc_collect", &[]);
818818
return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)));
819819
}
820+
"getAppVersion" if args.is_empty() => {
821+
let version = ctx.app_metadata.version.clone();
822+
let idx = ctx.strings.intern(&version);
823+
let handle_global = format!("@{}", ctx.strings.entry(idx).handle_global);
824+
return Ok(ctx.block().load(DOUBLE, &handle_global));
825+
}
826+
"getAppBuildNumber" if args.is_empty() => {
827+
return Ok(double_literal(ctx.app_metadata.build_number as f64));
828+
}
829+
"getBundleId" if args.is_empty() => {
830+
let bundle_id = ctx.app_metadata.bundle_id.clone();
831+
let idx = ctx.strings.intern(&bundle_id);
832+
let handle_global = format!("@{}", ctx.strings.entry(idx).handle_global);
833+
return Ok(ctx.block().load(DOUBLE, &handle_global));
834+
}
820835
// JSX runtime calls: `jsx(type, props)` and `jsxs(type, props)`.
821836
// The HIR lowers <div>…</div> to ExternFuncRef { name: "jsx" } and
822837
// <div><a/><b/></div> (multiple children) to "jsxs". The first arg

crates/perry-codegen/src/lower_call/native.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,26 @@ pub(crate) fn lower_native_method_call(
10571057
if method == "notificationSchedule" {
10581058
return lower_notification_schedule(ctx, args);
10591059
}
1060+
if args.is_empty() {
1061+
match method {
1062+
"getAppVersion" => {
1063+
let version = ctx.app_metadata.version.clone();
1064+
let idx = ctx.strings.intern(&version);
1065+
let handle_global = format!("@{}", ctx.strings.entry(idx).handle_global);
1066+
return Ok(ctx.block().load(DOUBLE, &handle_global));
1067+
}
1068+
"getAppBuildNumber" => {
1069+
return Ok(double_literal(ctx.app_metadata.build_number as f64));
1070+
}
1071+
"getBundleId" => {
1072+
let bundle_id = ctx.app_metadata.bundle_id.clone();
1073+
let idx = ctx.strings.intern(&bundle_id);
1074+
let handle_global = format!("@{}", ctx.strings.entry(idx).handle_global);
1075+
return Ok(ctx.block().load(DOUBLE, &handle_global));
1076+
}
1077+
_ => {}
1078+
}
1079+
}
10601080
if let Some(sig) = perry_system_table_lookup(method) {
10611081
return lower_perry_ui_table_call(ctx, sig, args);
10621082
}

crates/perry-dispatch/src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2228,6 +2228,24 @@ pub static PERRY_SYSTEM_TABLE: &[MethodRow] = &[
22282228
args: &[],
22292229
ret: ReturnKind::Str,
22302230
},
2231+
MethodRow {
2232+
method: "getAppVersion",
2233+
runtime: "perry_system_get_app_version",
2234+
args: &[],
2235+
ret: ReturnKind::Str,
2236+
},
2237+
MethodRow {
2238+
method: "getAppBuildNumber",
2239+
runtime: "perry_system_get_app_build_number",
2240+
args: &[],
2241+
ret: ReturnKind::F64,
2242+
},
2243+
MethodRow {
2244+
method: "getBundleId",
2245+
runtime: "perry_system_get_bundle_id",
2246+
args: &[],
2247+
ret: ReturnKind::Str,
2248+
},
22312249
MethodRow {
22322250
method: "getAppIcon",
22332251
runtime: "perry_system_get_app_icon",

0 commit comments

Comments
 (0)