Compare commits

..

39 Commits

Author SHA1 Message Date
Will Anderson 3f83adf458 ci: retrigger after ci-base image rebuild
El CI -dev / build-and-test (pull_request) Failing after 23s
2026-05-04 20:15:52 -05:00
Will Anderson 9f734b037c add comprehensive native test suite
El CI -dev / build-and-test (pull_request) Failing after 21s
2026-05-04 19:58:23 -05:00
Will Anderson 049a7712f4 ci: add -lm and allow-multiple-definition for bootstrap build
El CI -dev / build-and-test (pull_request) Failing after 54s
el_runtime.c uses pow/sqrt/log/sin/cos/exp - needs -lm.
elc-bootstrap.c predates the text-processing primitives commit so it
has its own C definitions of is_digit/is_whitespace; -Wl,--allow-multiple-definition
lets the linker accept both (equivalent implementations).
2026-05-04 16:08:47 -05:00
Will Anderson c64cbd21e2 retrigger - capture build error
El CI -dev / build-and-test (pull_request) Failing after 2m4s
2026-05-04 16:00:49 -05:00
Will Anderson 37488e9485 retrigger CI debug
El CI -dev / build-and-test (pull_request) Failing after 35s
2026-05-04 15:56:17 -05:00
Will Anderson 8641b4045e retrigger CI - free capacity
El CI -dev / build-and-test (pull_request) Failing after 1m1s
2026-05-04 15:53:20 -05:00
Will Anderson 49d68fbb20 retrigger CI - fix gitea DNS on host
El CI -dev / build-and-test (pull_request) Failing after 59s
2026-05-04 15:50:28 -05:00
Will Anderson 77a0658d56 retrigger CI attempt 5
El CI -dev / build-and-test (pull_request) Failing after 13m51s
2026-05-04 15:32:11 -05:00
Will Anderson bff0ad4f22 retrigger CI attempt 4
El CI -dev / build-and-test (pull_request) Failing after 32s
2026-05-04 15:30:08 -05:00
Will Anderson 49f96126b2 retrigger CI attempt 3
El CI -dev / build-and-test (pull_request) Failing after 39s
2026-05-04 15:27:52 -05:00
Will Anderson c954142063 retrigger CI after runner restart
El CI -dev / build-and-test (pull_request) Failing after 36s
2026-05-04 15:24:58 -05:00
Will Anderson 3fd5fec965 retrigger CI
El CI -dev / build-and-test (pull_request) Failing after 7s
2026-05-04 15:23:36 -05:00
Will Anderson 5476cbb2b1 ci: fix YAML - remove colon in step name, replace em dashes
El CI -dev / build-and-test (pull_request) Failing after 1s
2026-05-04 14:33:30 -05:00
Will Anderson 65792f7e4c ci: add workflow_dispatch trigger 2026-05-04 14:32:37 -05:00
Will Anderson c09023003d ci: retrigger after workflow bootstrap 2026-05-04 14:23:34 -05:00
Will Anderson 15b9ccd9e2 ci: retrigger 2026-05-04 14:13:56 -05:00
Will Anderson 9163af81aa ci: trigger CI run 2026-05-04 14:11:00 -05:00
Will Anderson 3dababa4ad dist: update elc-new binary to match elc 2026-05-04 13:27:57 -05:00
Will Anderson 5888258c9f rebuild elc: reporter=json, line numbers, em-dash FAIL format
Rebuild elc binary from the feat/native-testing source to match the
full reporter implementation:
- Lexer tracks line numbers in every token via state (__lex_line)
- Parser propagates line numbers into TestDef and Assert AST nodes
- Text reporter: "  FAIL  <name> — <msg>" (em dash, stderr)
- JSON reporter (--reporter=json): newline-delimited JSON to stdout
  with suite_start, test_start, test_pass, test_fail, suite_end events
- All fields: file (basename), line (test block), assert_line, message
- Fixed point verified: gen2 == gen3

elc-combined.el regenerated from source.
2026-05-04 13:27:01 -05:00
Will Anderson a9dc38ed82 ci: add native El test step to dev pipeline 2026-05-04 13:26:09 -05:00
Will Anderson 4af2b687e1 feat: native test/assert system -- elc --test runs test blocks in El
Add test { } and assert to the El language: the parser recognises TestDef
and Assert nodes; the C and JS codegens emit inert no-ops in normal mode
and a full test runner (with RUN/PASS/FAIL output and non-zero exit on
failure) when invoked with --test. compiler.el wires up compile_test /
compile_js_test and exposes --test / --reporter flags in the CLI.

Add two native test suites under tests/native/ (test_text.el,
test_codegen_js.el) covering string primitives, arithmetic, and list
operations. All 22 new native tests pass; the four existing run.sh
acceptance corpora are unaffected.
2026-05-04 13:24:55 -05:00
Will Anderson 32f0cf7b5d Add html-page.el example and rebuild elc binary
examples/html-page.el demonstrates HTML template syntax:
- <!doctype html> prefix handling
- Attribute values (static and interpolated)
- {#each list as item} iteration
- Auto-escaped interpolation via {expr}
- Self-closing void elements (meta, br, etc.)

Rebuilt dist/platform/elc from modified compiler source. The new
binary is self-hosted from the HTML-capable compiler source and
passes the standard identity check.
2026-05-04 13:02:54 -05:00
Will Anderson 65e26cd7a5 Add HTML template codegen and runtime for JS backend
JS codegen (codegen-js.el):
- js_cg_html_template: emits an IIFE that builds HTML via += concat
- js_cg_html_element_str / js_cg_html_parts / js_cg_html_attrs_str:
  mirror the C codegen structure using JS string accumulator
- js_cg_html_each: {#each} compiles to a JS for-loop
- Reuses existing js_str_lit / js_escape from the file header

Runtime (el_runtime.js):
- html_escape(s): replaces & < > " ' using regex chains
- html_raw(s): identity function
- Both exported from the runtime module exports object
2026-05-04 13:02:50 -05:00
Will Anderson 1fd7cd5545 Add HTML template codegen and runtime for C backend
C codegen (codegen.el):
- cg_html_template: emits a GCC/Clang statement-expression that
  builds the HTML string via el_str_concat chains
- cg_html_element_str / cg_html_parts / cg_html_attrs_str: recursive
  element and attribute emitters
- cg_html_each: {#each} compiles to a C for-loop with el_list_get
- __html_counter state tracks unique accumulator variable names
- Handles both 'static' (raw string) and 'dynamic' (expr node) attrs
  matching the parser's attribute kind convention

Runtime (el_runtime.c / el_runtime.h):
- html_escape(s): escapes & < > " ' for safe interpolation
- html_raw(s): identity function for raw() bypass
- Both use the existing html_buf_t infrastructure from el_html_sanitize
2026-05-04 13:02:44 -05:00
Will Anderson 71689520b6 Add HTML template syntax to El parser
Adds native HTML template literals to the El parser. Templates are
detected in value position when Lt is followed by a known HTML tag
name (is_html_tag_name) or by '!' for <!doctype html>.

New parser helpers:
- is_html_tag_name / is_void_element: classify tag names
- parse_html_text_tokens: collect intertoken text content
- parse_html_attrs: parse name, name="val", name={expr} attributes
- parse_html_children: recursive children with {expr}, {#each} support
- parse_html_element / parse_html_template: entry points

Adds Hash token kind ('#') to lexer for {#each} block syntax.

AST nodes: HtmlTemplate, html:Element, html:Text, html:Interp,
html:Raw, html:Each, html:Doctype. Doctype flag is stored on the
root element node rather than as a separate AST layer.

HTML templates parse correctly after 'return' and as the sole
expression in a function body. The 'return' keyword is required when
other let bindings precede the template, as El has no newline-as-
statement-terminator and '<' would otherwise be parsed as comparison.
2026-05-04 13:02:36 -05:00
Will Anderson e858eab300 spec: update codegen-js.md to Phase 5, ~90% coverage
Status updated from Phase 4 ~80% to Phase 5 ~90%.

New sections:
- 7. Language features coverage table (supported vs stubbed)
- 7a. Phase 5 constructs: extern fn, anonymous functions, try/catch,
  method call on Any, URL imports -- each with emit shape examples
- 9. Roadmap updated: Phases 1-5 marked DONE, Phase 6 unblocked

Runtime builtin table updated to ~90 builtins including all Phase 5
additions (promise_then/catch/resolve/reject, object_assign/keys/values,
json_deep_clone, array_from, type_of, instanceof_check).
2026-05-04 11:03:47 -05:00
Will Anderson aa7d97d5ba examples: rewrite browser-auth.el using new language features
No native_js or native_js_call anywhere. Full browser auth flow expressed
with proper El constructs:

- extern fn supabase_create_client(url, key) -> Any
  Declares the Supabase CDN global without an El function body.

- client.auth.signInWithOtp(opts)
  Direct method call chain on Any-typed value. The client is built by
  calling the extern fn; .auth field access and .signInWithOtp(opts)
  method call emit clean JS without any escape hatch.

- try { ... } catch (err: Any) { ... }
  Wraps the auth call; unexpected runtime errors are caught and shown
  to the user rather than crashing silently.

- fn(event: Any) -> Void { ... }
  Inline anonymous function literals for DOM event listeners instead
  of named forward-declared callbacks.

The rewrite is the proof: every browser JavaScript pattern used in a
real auth flow can now be expressed structurally in El.
2026-05-04 11:02:13 -05:00
Will Anderson 7040830470 codegen-js: URL import declarations for JS modules
import "https://cdn.example.com/lib.js" now emits:
  - module mode: import "https://..." at the top of the generated file
  - bundle/IIFE mode: // external: https://... comment

El source imports (.el files) are excluded -- they were already inlined
by resolve_imports before codegen. Any import path that doesn't end in
.el or starts with http(s):// is treated as an external JS dependency.
2026-05-04 11:01:36 -05:00
Will Anderson 3a513aaa5a runtime + codegen-js: Promise helpers and object/array utilities
Add to el_runtime.js:
  promise_then(p, cb)    -- p.then(cb), works with any Promise-returning API
  promise_catch(p, cb)   -- p.catch(cb)
  promise_resolve(val)   -- Promise.resolve(val)
  promise_reject(msg)    -- Promise.reject(new Error(msg))
  object_assign(t, s)    -- Object.assign({}, t, s) (non-mutating)
  object_keys(obj)       -- Object.keys(obj)
  object_values(obj)     -- Object.values(obj)
  json_deep_clone(obj)   -- JSON.parse(JSON.stringify(obj))
  array_from(iterable)   -- Array.from(iterable)
  type_of(val)           -- typeof val
  instanceof_check(v, n) -- val instanceof globalThis[name]

All new functions added to __el export object and ES named exports.
codegen-js preamble destructure updated to include all new names.
2026-05-04 11:01:14 -05:00
Will Anderson beb2a8c5bd lexer + parser + codegen: try/catch statement
try { ... } catch (name: Type) { ... } is now a first-class El statement.

Lexer: `try` and `catch` are now keywords (Try, Catch token kinds).
Parser: TryCatch AST node with try_body, catch_name, catch_body.
codegen-js: emits try { ... } catch (name) { ... } directly -- correct
  for all browser error handling patterns.
codegen.el (C backend): emits the try body with a comment; exception
  handling is a no-op since C has no analogous mechanism. Programs using
  try/catch should compile with --target=js.

The catch variable type annotation is parsed and skipped (same treatment
as all other type annotations in El).
2026-05-04 11:00:24 -05:00
Will Anderson e23319fe0b parser + codegen-js: anonymous function literals (lambda syntax)
fn(params) -> RetType { body } is now valid in expression position.
The parser produces a Lambda AST node. codegen-js emits a hoisted
JS function declaration with a generated name (__lambda_N) and returns
the name as the expression value, so inline callbacks compose cleanly:

  dom_listen(btn, "click", fn(event: Any) -> Void { handle(event) })

emits:

  function __lambda_1(event) { handle(event); }
  dom_listen(btn, "click", __lambda_1);

The hoisted-declaration strategy is debuggable, has no closure-capture
issues, and requires no string-buffer mode in the codegen.
2026-05-04 10:59:17 -05:00
Will Anderson 01fee9396a codegen-js: native JS method dispatch and extern fn support
Any-typed receiver method calls now emit obj.method(args) directly
instead of requiring native_js_call. client.auth.signInWithOtp(p)
compiles to client["auth"].signInWithOtp(p) -- no escape hatch needed.

Field access emits obj["field"] (direct bracket notation) instead of
el_get_field, so prototype-inherited JS properties resolve correctly.
el_get_field's hasOwnProperty guard was silently returning null for
real JS objects with inherited fields (Supabase auth, DOM APIs, etc).

El runtime shortform methods (append, len, get, map_get, map_set)
still use the existing method(obj, args) convention for backward compat.

ExternFn statements emit a comment and are excluded from top-level
statement codegen -- the extern declaration tells the compiler the
function exists in the JS environment without emitting a body.
2026-05-04 10:58:07 -05:00
Will Anderson 7b60d94b8a add --minify and --obfuscate flags to elc JS pipeline
Adds two post-processing flags that produce production-ready browser JS in a
single elc invocation, replacing extract-js.py in the web product pipeline:

  elc --target=js --bundle --minify source.el > output.min.js
  elc --target=js --bundle --obfuscate source.el > output.obf.js

--minify shells out to terser (passes=2, no drop_console, drop_debugger).
--obfuscate shells out to javascript-obfuscator with the same options as the
old extract-js.py script. --obfuscate implies --minify.

Tool discovery: checks ./node_modules/.bin/, ../node_modules/.bin/ (monorepo),
then falls back to npx. Both flags require --target=js; passing either without
it exits 1 with a clear error.

Both tools receive a reserved-names list of globals referenced from HTML
onclick= attributes (neuronDemoToggle, signInWith, NEURON_CFG, etc.) so they
are not mangled.

Implementation adds stdout_to_file(path)/stdout_restore() builtins to the C
runtime so codegen's println-streamed output can be captured to a temp file
before being piped through the external tools. Temp files use
/tmp/elc-<pid>-<timestamp>.js naming and are cleaned up on success and failure.

Rebuilds dist/platform/elc and dist/platform/elc.c. Self-hosting verified.
2026-05-04 10:54:34 -05:00
Will Anderson 21694b79d2 implement ? nil-propagation, write browser-auth.el example, update spec
Iteration 5:

? nil-propagation: Field and Index handlers in js_cg_expr now detect when
the object expression is a Try node (the AST node for postfix `?`).
When detected, emit JS optional chaining: `(expr)?.["field"] ?? null`.
The `?? null` normalizes JS undefined to El's null. A bare `expr?` not
followed by field/index still passes through unchanged.

browser-auth.el: a realistic 130-line example demonstrating:
  - @async function with Supabase via native_js_call
  - DOM bridge: get/set value/text/attr, add/remove class, show/hide
  - local_storage_get/set for session hints
  - window_on_load for initialization
  - window_set to expose functions to the browser global scope
  - set_timeout for transient state, is_valid_email for input validation
  Compiles cleanly with elc --target=js --bundle

Spec updated: status promoted to Phase 4 / ~80% coverage, nil-prop
status updated, new example referenced.
2026-05-04 10:42:54 -05:00
Will Anderson 422442b14e add --bundle flag for self-contained IIFE output
elc --target=js --bundle source.el > output.js produces a single file
with no import statement that can drop directly into a <script> tag.

How it works:
  - detect_bundle() reads the --bundle flag from argv
  - resolve_runtime_path() looks for el_runtime.js next to the source file
  - compile_js_with_bundle() reads the runtime, calls codegen_js_bundle()
  - codegen_js_inner(bundle_mode=true):
    - emits ;(function() { "use strict"; at the top
    - inlines the runtime content (stripping ES export statements which
      are invalid inside an IIFE via js_strip_es_exports())
    - skips the const {...} = globalThis.__el destructure -- the inlined
      function declarations are already in scope within the IIFE
    - closes with })(); after main()

Usage: elc --target=js --bundle app.el > app.js
       Place el_runtime.js in the same directory as app.el.
2026-05-04 10:40:46 -05:00
Will Anderson 437ba0a4dd add 20 browser API builtins to JS runtime and codegen preamble
Iteration 3: closes the browser API gap needed for real web pages.

New builtins in el_runtime.js:
  Extended DOM: dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html,
    dom_get_html, dom_get_parent, dom_contains_class, dom_get_checked,
    dom_set_checked
  Timers: set_timeout, set_interval, clear_interval
  Local storage: local_storage_get, local_storage_set, local_storage_remove
  Window: window_location, window_redirect, window_on_load
  Debug: console_log

All browser-only functions use _ensureBrowser guard. Timer functions
work in both Node and browser. All new names added to __el export
object, ES named exports, and codegen-js.el destructure preamble.
Spec table updated to document new categories.
2026-05-04 10:38:20 -05:00
Will Anderson 7376349124 fix TypeDef parser to consume optional = before field block
type User = { name: String } was silently broken: the parser consumed
the type name then called expect(LBrace) while sitting on the = token.
expect() advances unconditionally on mismatch, so it consumed = and
treated { as the first field name, producing a corrupt TypeDef node.

The FnDef following the broken TypeDef was then parsed incorrectly or
lost entirely -- causing greet() and similar functions to vanish from
JS/C output with no error.

Fix: detect and skip the optional Eq token before expecting LBrace.
Both targets benefit; rebuild elc to pick up the fix.
2026-05-04 10:36:53 -05:00
Will Anderson 0f1da43a97 implement Enum::Variant match patterns in parser and both codegens
Parser now handles `SomeEnum::Variant` in match arm patterns, emitting
a Variant pattern node with enum_name and variant fields. Previously
these fell through to Binding, producing broken codegen.

JS codegen: emit str_eq check against the variant name string (El enums
are plain strings at runtime). C codegen: same, via EL_STR + str_eq.

Rebuild elc to pick up the parser change.
2026-05-04 10:35:35 -05:00
Will Anderson a54b2bebf9 add DOM bridge, async/await, window export, and native_js to JS target
- el_runtime.js: add 19 dom_* builtins (browser-only, throw in Node),
  window_set/window_get for exposing El functions to the browser global
  scope, and native_js/native_js_call escape hatches for third-party libs
- codegen-js.el: destructure all new builtins in generated preamble; add
  @async decorator support that emits async function + await at call sites
  for known-async HTTP builtins and user-declared @async functions; pre-
  registration pass ensures forward calls to @async functions get await
- spec/codegen-js.md: mark Phase 3 (DOM bridge) implemented, document
  @async approach and its limitations, update builtin table and status
- examples/browser-counter.el: canonical example showing dom_get_element,
  dom_set_text, dom_is_null, window_set, and state_set/get
2026-05-04 10:29:43 -05:00
586 changed files with 5538 additions and 273531 deletions
+45 -255
View File
@@ -1,4 +1,4 @@
name: El SDK CI - dev
name: El CI -dev
on:
push:
@@ -7,13 +7,11 @@ on:
pull_request:
branches:
- dev
workflow_dispatch:
jobs:
build-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: lang
steps:
- name: Checkout
@@ -22,306 +20,98 @@ jobs:
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev apt-transport-https ca-certificates
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" \
> /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
apt-get install -y gcc libcurl4-openssl-dev
# Seed: use the committed linux-amd64 binary as the bootstrap
- name: Bootstrap from committed linux binary (seed)
# Gen2: compile the bootstrap C source into a working elc binary
# -Wl,--allow-multiple-definition: is_digit/is_whitespace exist in both
# elc-bootstrap.c (pre-dates runtime text primitives) and el_runtime.c.
# Both definitions are equivalent; allow the linker to pick one.
- name: Build elc from bootstrap (gen2)
run: |
chmod +x dist/platform/elc-linux-amd64
echo "seed elc (committed linux-amd64 binary)"
dist/platform/elc-linux-amd64 --version || true
# Gen2: use seed to self-host compile the El compiler
- name: Self-host compile El compiler (gen2)
run: |
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-gen2.c \
dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-lcurl -lpthread -lm \
-Wl,--allow-multiple-definition \
-o dist/elc-gen2
chmod +x dist/elc-gen2
echo "gen2 elc built"
dist/elc-gen2 --version || true
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
- name: Self-host compile El compiler with gen2 (gen3)
run: |
mkdir -p dist/platform
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-gen3.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread -lm \
-o dist/platform/elc
chmod +x dist/platform/elc
echo "gen2 (self-hosted) elc built"
echo "gen3 (self-hosted) elc built"
dist/platform/elc --version || true
# Build elb (needed for Artifact Registry publish and downstream CI)
- name: Build elb
run: |
mkdir -p dist/bin
dist/platform/elc elb.el > dist/elb.c
gcc -O2 \
-I el-compiler/runtime \
dist/elb.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-o dist/bin/elb
chmod +x dist/bin/elb
echo "elb built"
- name: Run tests - text
# Run all four test suites -all must pass
- name: Run tests -text
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/text/run.sh
- name: Run tests - calendar
- name: Run tests -calendar
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/calendar/run.sh
- name: Run tests - time
- name: Run tests -time
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/time/run.sh
- name: Run tests - html_sanitizer
- name: Run tests -html_sanitizer
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/html_sanitizer/run.sh
# Native El test suites (elc --test, compile-link-run)
# el_runtime.c is precompiled to .o once and reused by all 8 modules.
- name: Precompile el_runtime.o
run: |
set -euo pipefail
RUNTIME="$(pwd)/el-compiler/runtime"
gcc -O2 -c -I "$RUNTIME" "$RUNTIME/el_runtime.c" \
-o /tmp/el_runtime.o
echo "el_runtime.o compiled"
- name: Run tests - native (core)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core
- name: Run tests - native (text)
- name: Run tests -native (text)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text
- name: Run tests - native (string)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string
- name: Run tests - native (math)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math
- name: Run tests - native (state)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state
- name: Run tests - native (time)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time
- name: Run tests - native (json)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json
- name: Run tests - native (env)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env
- name: Run tests - native (fs)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c /tmp/el_runtime.o \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
/tmp/el_native_fs
# Build epm binary using elb (epm lives at repo root, not inside lang/)
- name: Build epm
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/epm
echo "epm built"
# Build el-install binary using elb
- name: Build el-install
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/el-install
echo "el-install built"
# Publish only after merge (push event), not on PR validation runs
- name: Publish El SDK to Artifact Registry (dev)
if: github.event_name == 'push'
# Publish artifact to GCP Artifact Registry (dev)
- name: Publish elc to Artifact Registry (dev)
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
# Fail loudly: previously this step had no `set -e`, so an auth or
# upload failure was swallowed (step exited 0 on the trailing echo)
# and the SDK silently never published. Surface failures now.
set -euo pipefail
if [ -z "${GCP_SA_KEY:-}" ]; then
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
exit 1
fi
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
VERSION="${GITHUB_SHA:0:8}"
VERSION="${GITEA_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-elc \
--package=el/elc \
--version="${VERSION}" \
--source=dist/platform/elc
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-elb \
--version="${VERSION}" \
--source=dist/bin/elb
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-h \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h
gcloud artifacts generic upload \
--repository=foundation-dev \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-js \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.js
echo "Published El SDK version=${VERSION} to foundation-dev"
# Keep key alive for the ci-base rebuild step below
# (deleted in that step after docker push)
- name: Rebuild ci-base with fresh El SDK (dev)
# Patches ci-base:dev in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
#
# continue-on-error: this is a CI-cache optimization, NOT the release
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
# runner where DinD/Docker availability is fragile. A failure here must
# never block or redden the job — the SDK publish above is the deliverable.
continue-on-error: true
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
set -euo pipefail
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
SHA="${GITHUB_SHA:0:8}"
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# Pull existing ci-base:dev (or fall back to :latest on first run)
BASE_TAG="dev"
docker pull "${CI_BASE}:dev" || { docker pull "${CI_BASE}:latest" && BASE_TAG="latest"; }
# Inline Dockerfile — only replaces the El SDK layer
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
ARG BASE
FROM ${BASE}
COPY dist/platform/elc /opt/el/dist/platform/elc
COPY dist/bin/elb /opt/el/dist/bin/elb
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
EOF
docker build \
--build-arg BASE="${CI_BASE}:${BASE_TAG}" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-f /tmp/Dockerfile.ci-base-patch \
-t "${CI_BASE}:dev" \
-t "${CI_BASE}:dev-${SHA}" \
.
docker push "${CI_BASE}:dev"
docker push "${CI_BASE}:dev-${SHA}"
echo "ci-base rebuilt: ${CI_BASE}:dev (${SHA})"
# Also tag as latest-dev
echo "Published elc version=${VERSION} to foundation-dev/el/elc"
rm -f /tmp/gcp-key.json
+34 -245
View File
@@ -1,4 +1,4 @@
name: El SDK CI - stage
name: El CI stage
on:
push:
@@ -11,301 +11,90 @@ on:
jobs:
build-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: lang
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Enforce source branch (stage <- dev only)
if: github.event_name == 'pull_request'
run: |
SOURCE="${GITHUB_HEAD_REF}"
if [ "${SOURCE}" != "dev" ]; then
echo "ERROR: Stage branch only accepts PRs from 'dev'. Source was: '${SOURCE}'"
exit 1
fi
echo "Source branch check passed: ${SOURCE} -> stage"
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
# Seed: use the committed linux-amd64 binary as the bootstrap
- name: Bootstrap from committed linux binary (seed)
# Gen2: compile the bootstrap C source into a working elc binary
- name: Build elc from bootstrap (gen2)
run: |
chmod +x dist/platform/elc-linux-amd64
echo "seed elc (committed linux-amd64 binary)"
dist/platform/elc-linux-amd64 --version || true
# Gen2: use seed to self-host compile the El compiler
- name: Self-host compile El compiler (gen2)
run: |
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-gen2.c \
dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-lcurl -lpthread \
-o dist/elc-gen2
chmod +x dist/elc-gen2
echo "gen2 elc built"
dist/elc-gen2 --version || true
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
- name: Self-host: compile El compiler with gen2 (gen3)
run: |
mkdir -p dist/platform
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-gen3.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-o dist/platform/elc
chmod +x dist/platform/elc
echo "gen2 (self-hosted) elc built"
echo "gen3 (self-hosted) elc built"
dist/platform/elc --version || true
- name: Run tests - text
# Run all four test suites — all must pass
- name: Run tests — text
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/text/run.sh
- name: Run tests - calendar
- name: Run tests calendar
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/calendar/run.sh
- name: Run tests - time
- name: Run tests time
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/time/run.sh
- name: Run tests - html_sanitizer
- name: Run tests html_sanitizer
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/html_sanitizer/run.sh
# Native El test suites (elc --test, compile-link-run)
- name: Run tests - native (core)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core
- name: Run tests - native (text)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text
- name: Run tests - native (string)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string
- name: Run tests - native (math)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math
- name: Run tests - native (state)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state
- name: Run tests - native (time)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time
- name: Run tests - native (json)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json
- name: Run tests - native (env)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env
- name: Run tests - native (fs)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
/tmp/el_native_fs
# Build elb (needed for epm and el-install builds below)
- name: Build elb
run: |
mkdir -p dist/bin
dist/platform/elc elb.el > dist/elb.c
gcc -O2 \
-I el-compiler/runtime \
dist/elb.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-o dist/bin/elb
chmod +x dist/bin/elb
echo "elb built"
# Build epm binary using elb (epm lives at repo root, not inside lang/)
- name: Build epm
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/epm
echo "epm built"
# Build el-install binary using elb
- name: Build el-install
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/el-install
echo "el-install built"
# Publish only after merge (push event), not on PR validation runs
- name: Publish El SDK to Artifact Registry (stage)
if: github.event_name == 'push'
# Publish artifact to GCP Artifact Registry (stage)
- name: Publish elc to Artifact Registry (stage)
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
# Fail loudly: previously this step had no `set -e`, so an auth or
# upload failure was swallowed (step exited 0 on the trailing echo)
# and the SDK silently never published. Surface failures now.
set -euo pipefail
if [ -z "${GCP_SA_KEY:-}" ]; then
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
exit 1
fi
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates curl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
VERSION="${GITHUB_SHA:0:8}"
VERSION="${GITEA_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el-elc \
--package=el/elc \
--version="${VERSION}" \
--source=dist/platform/elc
gcloud artifacts generic upload \
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
gcloud artifacts generic upload \
--repository=foundation-stage \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-h \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h
echo "Published El SDK version=${VERSION} to foundation-stage"
# Keep key alive for the ci-base rebuild step below
# (deleted in that step after docker push)
- name: Rebuild ci-base with fresh El SDK (stage)
# Patches ci-base:stage in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
#
# continue-on-error: this is a CI-cache optimization, NOT the release
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
# runner where DinD/Docker availability is fragile. A failure here must
# never block or redden the job — the SDK publish above is the deliverable.
continue-on-error: true
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
set -euo pipefail
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
SHA="${GITHUB_SHA:0:8}"
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# Pull existing ci-base:stage (system deps stay cached in the base layer)
docker pull "${CI_BASE}:stage" || docker pull "${CI_BASE}:latest"
# Inline Dockerfile — only replaces the El SDK layer
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
ARG BASE
FROM ${BASE}
COPY dist/platform/elc /opt/el/dist/platform/elc
COPY dist/bin/elb /opt/el/dist/bin/elb
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
EOF
docker build \
--build-arg BASE="${CI_BASE}:stage" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-f /tmp/Dockerfile.ci-base-patch \
-t "${CI_BASE}:stage" \
-t "${CI_BASE}:stage-${SHA}" \
.
docker push "${CI_BASE}:stage"
docker push "${CI_BASE}:stage-${SHA}"
echo "ci-base rebuilt: ${CI_BASE}:stage (${SHA})"
echo "Published elc version=${VERSION} to foundation-stage/el/elc"
rm -f /tmp/gcp-key.json
+72 -301
View File
@@ -4,234 +4,81 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-and-release:
runs-on: ubuntu-latest
defaults:
run:
working-directory: lang
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Enforce source branch (main <- stage only)
if: github.event_name == 'pull_request'
run: |
SOURCE="${GITHUB_HEAD_REF}"
if [ "${SOURCE}" != "stage" ]; then
echo "ERROR: Main branch only accepts PRs from 'stage'. Source was: '${SOURCE}'"
exit 1
fi
echo "Source branch check passed: ${SOURCE} -> main"
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
# Seed: use the committed linux-amd64 binary as the bootstrap
- name: Bootstrap from committed linux binary (seed)
# Gen2: compile the bootstrap C source into a working elc binary
- name: Build elc from bootstrap (gen2)
run: |
chmod +x dist/platform/elc-linux-amd64
echo "seed elc (committed linux-amd64 binary)"
dist/platform/elc-linux-amd64 --version || true
gcc -O2 \
-I el-compiler/runtime \
dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \
-o dist/elc-gen2
chmod +x dist/elc-gen2
echo "gen2 elc built"
dist/elc-gen2 --version || true
# Gen2: use seed to self-host compile the El compiler
- name: Self-host compile El compiler (gen2)
# Gen3: use gen2 to compile the El compiler from its own El source (self-host)
- name: Self-host: compile El compiler with gen2 (gen3)
run: |
mkdir -p dist/platform
dist/platform/elc-linux-amd64 elc-cli.el > dist/elc-gen2.c
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
gcc -O2 \
-I el-compiler/runtime \
dist/elc-gen2.c \
dist/elc-gen3.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-lcurl -lpthread \
-o dist/platform/elc
chmod +x dist/platform/elc
echo "gen2 (self-hosted) elc built"
echo "gen3 (self-hosted) elc built"
dist/platform/elc --version || true
# Build elb binary
- name: Build elb
run: |
mkdir -p dist/bin
dist/platform/elc elb.el > dist/elb.c
gcc -O2 \
-I el-compiler/runtime \
dist/elb.c \
el-compiler/runtime/el_runtime.c \
-lcurl -lssl -lcrypto -lpthread -lm \
-o dist/bin/elb
chmod +x dist/bin/elb
echo "elb built"
# Build epm binary using elb (epm lives at repo root, not inside lang/)
- name: Build epm
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd ../epm && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/epm
echo "epm built"
# Build el-install binary using elb
- name: Build el-install
run: |
ABS_ELB="$(pwd)/dist/bin/elb"
ABS_ELC="$(pwd)/dist/platform/elc"
ABS_RUNTIME="$(pwd)/el-compiler/runtime"
ABS_OUT="$(pwd)/dist/bin"
(cd tools/install && "$ABS_ELB" --clean --elc="$ABS_ELC" --runtime="$ABS_RUNTIME" --out="$ABS_OUT")
chmod +x dist/bin/el-install
echo "el-install built"
- name: Run tests - text
# Run all four test suites with gen3 elc
- name: Run tests — text
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/text/run.sh
- name: Run tests - calendar
- name: Run tests calendar
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/calendar/run.sh
- name: Run tests - time
- name: Run tests time
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/time/run.sh
- name: Run tests - html_sanitizer
- name: Run tests html_sanitizer
run: |
ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \
bash tests/html_sanitizer/run.sh
# Native El test suites (elc --test, compile-link-run)
- name: Run tests - native (core)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_core.el > /tmp/el_native_core.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_core.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core
- name: Run tests - native (text)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_text.el > /tmp/el_native_text.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_text.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text
- name: Run tests - native (string)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_string.el > /tmp/el_native_string.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_string.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string
- name: Run tests - native (math)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_math.el > /tmp/el_native_math.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_math.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math
- name: Run tests - native (state)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_state.el > /tmp/el_native_state.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_state.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state
- name: Run tests - native (time)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_time.el > /tmp/el_native_time.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_time.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time
- name: Run tests - native (json)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_json.el > /tmp/el_native_json.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_json.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json
- name: Run tests - native (env)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_env.el > /tmp/el_native_env.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_env.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env
- name: Run tests - native (fs)
run: |
set -euo pipefail
ELC="$(pwd)/dist/platform/elc"
RUNTIME="$(pwd)/el-compiler/runtime"
"$ELC" --test tests/native/test_fs.el > /tmp/el_native_fs.c
gcc -O2 -I "$RUNTIME" /tmp/el_native_fs.c "$RUNTIME/el_runtime.c" \
-lcurl -lssl -lcrypto -lpthread -lm -o /tmp/el_native_fs
/tmp/el_native_fs
# Bundle the SDK tarball - runs from the repo root to reference lang/ paths correctly
- name: Bundle SDK tarball
if: github.event_name == 'push'
working-directory: ${{ github.workspace }}
run: |
mkdir -p dist/sdk/bin dist/sdk/runtime
cp lang/dist/platform/elc dist/sdk/bin/elc
cp lang/dist/bin/elb dist/sdk/bin/elb
cp lang/dist/bin/epm dist/sdk/bin/epm
cp lang/el-compiler/runtime/el_runtime.c dist/sdk/runtime/
cp lang/el-compiler/runtime/el_runtime.h dist/sdk/runtime/
cp lang/runtime/*.el dist/sdk/runtime/
tar -czf dist/el-sdk-latest.tar.gz -C dist/sdk .
echo "SDK tarball bundled: dist/el-sdk-latest.tar.gz"
ls -lh dist/el-sdk-latest.tar.gz
# Publish / update the `latest` release with all SDK assets
# Publish / update the `latest` release with the three SDK assets
- name: Publish latest release
if: github.event_name == 'push'
working-directory: ${{ github.workspace }}
env:
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1
REPO: neuron-technologies/el
run: |
# Delete existing `latest` release if it exists
EXISTING_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/repos/${REPO}/releases/tags/latest" \
@@ -244,10 +91,12 @@ jobs:
"${GITEA_API}/repos/${REPO}/releases/${EXISTING_ID}"
fi
# Delete and re-create the `latest` tag so it points at HEAD
curl -sf -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/repos/${REPO}/tags/latest" || true
# Create the release
RELEASE_ID=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
@@ -262,6 +111,7 @@ jobs:
echo "Created release id=${RELEASE_ID}"
# Upload assets
upload_asset() {
local filepath="$1"
local name="$2"
@@ -272,149 +122,70 @@ jobs:
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets"
}
# Per-file assets (downstream CI needs these individually)
upload_asset lang/dist/platform/elc elc
upload_asset lang/el-compiler/runtime/el_runtime.c el_runtime.c
upload_asset lang/el-compiler/runtime/el_runtime.h el_runtime.h
# SDK bundle and installer binary
upload_asset dist/el-sdk-latest.tar.gz el-sdk-latest.tar.gz
upload_asset lang/dist/bin/el-install el-install
upload_asset dist/platform/elc elc
upload_asset el-compiler/runtime/el_runtime.c el_runtime.c
upload_asset el-compiler/runtime/el_runtime.h el_runtime.h
echo "Release published successfully"
- name: Publish El SDK to Artifact Registry (prod)
if: github.event_name == 'push'
# Dispatch el-sdk-updated event to downstream repos
# Publish artifact to GCP Artifact Registry (prod)
- name: Publish elc to Artifact Registry (prod)
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
# Fail loudly: previously this step had no `set -e`, so an auth or
# upload failure was swallowed (step exited 0 on the trailing echo)
# and the SDK silently never published. Surface failures now.
set -euo pipefail
if [ -z "${GCP_SA_KEY:-}" ]; then
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
exit 1
fi
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
apt-get install -y -qq apt-transport-https ca-certificates curl
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update -qq && apt-get install -y google-cloud-cli
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
VERSION="${GITHUB_SHA:0:8}"
VERSION="${GITEA_SHA:0:8}"
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-elc \
--package=el/elc \
--version="${VERSION}" \
--source=dist/platform/elc
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-elb \
--version="${VERSION}" \
--source=dist/bin/elb
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-c \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.c
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-h \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.h
gcloud artifacts generic upload \
--repository=foundation-prod \
--location=us-central1 \
--project=neuron-785695 \
--package=el-runtime-js \
--version="${VERSION}" \
--source=el-compiler/runtime/el_runtime.js
echo "Published El SDK version=${VERSION} to foundation-prod"
# Keep key alive for the ci-base rebuild step below
# (deleted in that step after docker push)
- name: Rebuild ci-base with fresh El SDK
# Patches ci-base:latest in-place: pulls the existing image (which has all
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
#
# continue-on-error: this is a CI-cache optimization, NOT the release
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
# runner where DinD/Docker availability is fragile. A failure here must
# never block or redden the job — the SDK publish above is the deliverable.
continue-on-error: true
if: github.event_name == 'push'
env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
run: |
set -euo pipefail
CI_BASE="us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base"
SHA="${GITHUB_SHA:0:8}"
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
gcloud config set project neuron-785695
gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
# Pull existing ci-base (system deps stay cached in the base layer)
docker pull "${CI_BASE}:latest"
# Inline Dockerfile — only replaces the El SDK layer
cat > /tmp/Dockerfile.ci-base-patch << 'EOF'
ARG BASE
FROM ${BASE}
COPY dist/platform/elc /opt/el/dist/platform/elc
COPY dist/bin/elb /opt/el/dist/bin/elb
COPY el-compiler/runtime/el_runtime.c /opt/el/el-compiler/runtime/el_runtime.c
COPY el-compiler/runtime/el_runtime.h /opt/el/el-compiler/runtime/el_runtime.h
COPY el-compiler/runtime/el_runtime.js /opt/el/el-compiler/runtime/el_runtime.js
RUN chmod +x /opt/el/dist/platform/elc /opt/el/dist/bin/elb
EOF
docker build \
--build-arg BASE="${CI_BASE}:latest" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-f /tmp/Dockerfile.ci-base-patch \
-t "${CI_BASE}:latest" \
-t "${CI_BASE}:${SHA}" \
.
docker push "${CI_BASE}:latest"
docker push "${CI_BASE}:${SHA}"
echo "ci-base rebuilt: ${CI_BASE}:latest (${SHA})"
echo "Published elc version=${VERSION} to foundation-prod/el/elc"
rm -f /tmp/gcp-key.json
- name: Dispatch el-sdk-updated to downstream repos
if: github.event_name == 'push'
- name: Dispatch to foundation/engram
env:
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1
run: |
for repo in neuron-technologies/forge neuron-technologies/neuron-web; do
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/repos/${repo}/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {\"el_version\": \"latest\", \"commit\": \"${GITHUB_SHA}\"}
}" && echo "Dispatched to ${repo}" || echo "Warning: dispatch to ${repo} failed"
done
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/repos/neuron-technologies/engram/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {
\"el_version\": \"latest\",
\"commit\": \"${GITHUB_SHA}\"
}
}"
echo "Dispatched el-sdk-updated to foundation/engram"
- name: Dispatch to neuron-technologies/forge
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1
run: |
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/repos/neuron-technologies/forge/dispatches" \
-d "{
\"type\": \"el-sdk-updated\",
\"inputs\": {
\"el_version\": \"latest\",
\"commit\": \"${GITHUB_SHA}\"
}
}"
echo "Dispatched el-sdk-updated to neuron-technologies/forge"
-50
View File
@@ -1,50 +0,0 @@
#!/usr/bin/env bash
# El pre-commit hook: compile and run native tests before commit.
# Install once per clone: git config core.hooksPath .githooks
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
LANG_DIR="$ROOT/lang"
RUNTIME="$LANG_DIR/el-compiler/runtime"
ELC="$LANG_DIR/dist/platform/elc"
# If elc isn't built yet, skip with a warning rather than blocking
if [ ! -x "$ELC" ]; then
echo "⚠ elc not found at lang/dist/platform/elc — skipping pre-commit tests"
echo " Build it first: cd lang && gcc -O2 -I el-compiler/runtime dist/elc-bootstrap.c el-compiler/runtime/el_runtime.c -lcurl -lpthread -o dist/elc-gen2 && ./dist/elc-gen2 el-compiler/src/compiler.el > /tmp/elc.c && gcc -O2 -I el-compiler/runtime /tmp/elc.c el-compiler/runtime/el_runtime.c -lcurl -lpthread -o dist/platform/elc"
exit 0
fi
echo "→ Running El native tests..."
PASS=0
FAIL=0
FAILED_TESTS=""
for test_file in "$LANG_DIR"/tests/native/test_*.el; do
name=$(basename "$test_file" .el)
tmp_c="/tmp/el_hook_${name}.c"
tmp_bin="/tmp/el_hook_${name}"
if "$ELC" --test "$test_file" > "$tmp_c" 2>/dev/null \
&& gcc -O2 -I "$RUNTIME" "$tmp_c" "$RUNTIME/el_runtime.c" \
-lcurl -lpthread -lm -o "$tmp_bin" 2>/dev/null \
&& "$tmp_bin" 2>/dev/null; then
PASS=$((PASS + 1))
else
echo " ✗ $name"
FAIL=$((FAIL + 1))
FAILED_TESTS="$FAILED_TESTS $name"
fi
done
echo " $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
echo ""
echo "✗ Pre-commit failed. Fix these tests before committing:$FAILED_TESTS"
exit 1
fi
echo "✓ All tests passed"
exit 0
View File
View File
-23
View File
@@ -1,23 +0,0 @@
// arbor-cli the `arbor` command-line tool.
// Inlines its own copies of the parse / layout / render pipeline so that the
// resulting binary is self-contained. (El's `import` form today concatenates
// source; once a real module loader lands this becomes a thin driver.)
vessel "arbor-cli" {
version "0.1.0"
description "Command-line interface for the Arbor diagram language"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
arbor-parse "0.1"
arbor-layout "0.1"
arbor-render "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
File diff suppressed because it is too large Load Diff
-18
View File
@@ -1,18 +0,0 @@
// arbor-core fundamental types for Arbor diagrams.
// Node IDs (sanitised), shape vocabulary, edge kinds, and the lightweight
// graph value used by every other vessel.
vessel "arbor-core" {
version "0.1.0"
description "Core types for Arbor diagrams: NodeId, ArborShape, ArborEdgeKind, graphs"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
}
build {
entry "src/main.el"
output "dist/"
}
-333
View File
@@ -1,333 +0,0 @@
// arbor-core core types for Arbor diagrams.
//
// Idiomatic El: everything is a Map. Functions take/return maps; helpers are
// pure and small. The downstream vessels (parse, layout, render) consume the
// shapes defined here.
//
// Shape vocabulary:
// ArborShape strings "rect" "rounded" "cylinder" "diamond" "stadium" "primary"
//
// Edge-kind strings:
// "solid" "dashed" "forbidden" "bidirectional"
//
// Node value: { "id":Str, "label":Str, "shape":Str }
// Edge value: { "from":Str, "to":Str, "label":Str, "kind":Str }
// Group value: { "id":Str, "label":Str, "node_ids":[Str], "direction":Str }
// Graph value: { "title":Str, "direction":Str, "nodes":[Node], "edges":[Edge], "groups":[Group] }
//
// Diagram-form (lowered) is the same shape but with NodeStyle/EdgeLine/Arrow
// resolved into renderer-friendly fields:
// Node: + "sublabel":Str, "style_fill":Str, "style_stroke":Str, "style_color":Str
// Edge: + "line":Str ("solid"/"dashed"/"dotted"/"thick"), "arrow":Str ("forward"/"backward"/"both"/"none")
//
// This file is the canonical definition of those shapes. Other vessels rely on
// these field names.
// NodeId sanitisation
//
// Sanitise an arbitrary string into a Mermaid-safe identifier.
// - any char not in [a-zA-Z0-9_] becomes '_'
// - consecutive underscores collapse
// - trailing underscores stripped
// - if first char is a digit, prepend 'n'
// - if empty, return "node"
fn is_alnum_underscore(ch: String) -> Bool {
let code: Int = str_char_code(ch, 0)
if code >= 48 {
if code <= 57 { return true }
}
if code >= 65 {
if code <= 90 { return true }
}
if code >= 97 {
if code <= 122 { return true }
}
if code == 95 { return true }
false
}
fn is_ascii_digit(ch: String) -> Bool {
let code: Int = str_char_code(ch, 0)
if code >= 48 {
if code <= 57 { return true }
}
false
}
fn sanitize_id(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "node" }
// Pass 1: replace and collapse.
let out = ""
let prev_underscore = false
let i = 0
while i < n {
let ch: String = str_char_at(s, i)
if is_alnum_underscore(ch) {
let out = out + ch
let prev_underscore = false
} else {
if !prev_underscore {
let out = out + "_"
}
let prev_underscore = true
}
let i = i + 1
}
// Pass 2: strip trailing underscores.
let m: Int = str_len(out)
let end = m
let stripping = true
while stripping {
if end <= 0 {
let stripping = false
} else {
let last: String = str_char_at(out, end - 1)
if last == "_" {
let end = end - 1
} else {
let stripping = false
}
}
}
let out = str_slice(out, 0, end)
if str_len(out) == 0 { return "node" }
// Pass 3: leading-digit guard.
let first: String = str_char_at(out, 0)
if is_ascii_digit(first) {
let out = "n" + out
}
out
}
// Constructors
fn make_node(id: String, label: String, shape: String) -> Map<String, Any> {
{ "id": id, "label": label, "shape": shape }
}
fn make_edge(src: String, dst: String, kind: String) -> Map<String, Any> {
{ "from": src, "to": dst, "label": "", "kind": kind }
}
fn make_edge_with_label(src: String, dst: String, kind: String, label: String) -> Map<String, Any> {
{ "from": src, "to": dst, "label": label, "kind": kind }
}
fn make_group(id: String, label: String) -> Map<String, Any> {
let empty_ids: [String] = el_list_empty()
{ "id": id, "label": label, "node_ids": empty_ids, "direction": "" }
}
fn make_graph() -> Map<String, Any> {
let empty_n: [Map<String, Any>] = el_list_empty()
let empty_e: [Map<String, Any>] = el_list_empty()
let empty_g: [Map<String, Any>] = el_list_empty()
{ "title": "", "direction": "top-down",
"nodes": empty_n, "edges": empty_e, "groups": empty_g }
}
// Shape vocabulary
// Returns the canonical shape string for a token, or "" if unknown.
fn shape_from_token(tok: String) -> String {
let t: String = str_trim(tok)
if t == "rect" { return "rect" }
if t == "rounded" { return "rounded" }
if t == "cylinder" { return "cylinder" }
if t == "diamond" { return "diamond" }
if t == "stadium" { return "stadium" }
if t == "primary" { return "primary" }
""
}
// Lower an Arbor shape into the renderer's NodeShape vocabulary.
fn shape_to_node_shape(shape: String) -> String {
if shape == "rect" { return "rectangle" }
if shape == "primary" { return "rectangle" }
if shape == "rounded" { return "rounded_rect" }
if shape == "cylinder" { return "cylinder" }
if shape == "diamond" { return "diamond" }
if shape == "stadium" { return "stadium" }
"rectangle"
}
// Lowering: ArborGraph DiagramGraph
//
// Replaces every node with a diagram-form node carrying explicit style fields,
// and every edge with a diagram-form edge carrying line/arrow strings.
fn lower_node(n: Map<String, Any>) -> Map<String, Any> {
let shape: String = n["shape"]
let node_shape: String = shape_to_node_shape(shape)
let fill = ""
let stroke = ""
let color = ""
if shape == "primary" {
let fill = "#0052A0"
let stroke = "#0052A0"
let color = "#ffffff"
}
{ "id": n["id"], "label": n["label"], "sublabel": "",
"shape": node_shape,
"style_fill": fill, "style_stroke": stroke, "style_color": color }
}
fn lower_edge(e: Map<String, Any>) -> Map<String, Any> {
let kind: String = e["kind"]
let line = "solid"
let arrow = "forward"
if kind == "dashed" {
let line = "dashed"
}
if kind == "bidirectional" {
let arrow = "both"
}
// forbidden uses solid line + forward arrow; the renderer overlays the
// circle-X marker based on a forbidden-set the caller threads through.
{ "from": e["from"], "to": e["to"], "label": e["label"],
"line": line, "arrow": arrow }
}
fn lower_graph(g: Map<String, Any>) -> Map<String, Any> {
let nodes: [Map<String, Any>] = g["nodes"]
let edges: [Map<String, Any>] = g["edges"]
let lowered_nodes: [Map<String, Any>] = el_list_empty()
let i = 0
let n: Int = el_list_len(nodes)
while i < n {
let lowered_nodes = native_list_append(lowered_nodes, lower_node(get(nodes, i)))
let i = i + 1
}
let lowered_edges: [Map<String, Any>] = el_list_empty()
let i = 0
let m: Int = el_list_len(edges)
while i < m {
let lowered_edges = native_list_append(lowered_edges, lower_edge(get(edges, i)))
let i = i + 1
}
{ "title": g["title"], "direction": g["direction"],
"nodes": lowered_nodes, "edges": lowered_edges, "groups": g["groups"] }
}
// Find a node by id within a (lowered or raw) graph. Returns an empty map
// when not found callers check map_get(result, "id") for presence.
fn graph_find_node(graph: Map<String, Any>, id: String) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let n: Int = el_list_len(nodes)
let i = 0
while i < n {
let node: Map<String, Any> = get(nodes, i)
let nid: String = node["id"]
if nid == id { return node }
let i = i + 1
}
let empty: Map<String, Any> = el_map_new(0)
empty
}
// Forbidden-edge set helpers
// The lowered graph drops the "forbidden" kind (line/arrow have no slot for
// it). Callers preserve the set as a list of "from->to" strings.
fn forbidden_key(from: String, to: String) -> String {
from + "->" + to
}
fn collect_forbidden(graph: Map<String, Any>) -> [String] {
let edges: [Map<String, Any>] = graph["edges"]
let n: Int = el_list_len(edges)
let out: [String] = el_list_empty()
let i = 0
while i < n {
let e: Map<String, Any> = get(edges, i)
let kind: String = e["kind"]
if kind == "forbidden" {
let f: String = e["from"]
let t: String = e["to"]
let out = native_list_append(out, forbidden_key(f, t))
}
let i = i + 1
}
out
}
fn forbidden_contains(set: [String], src: String, dst: String) -> Bool {
let key: String = forbidden_key(src, dst)
let n: Int = el_list_len(set)
let i = 0
while i < n {
let s: String = get(set, i)
if s == key { return true }
let i = i + 1
}
false
}
// Smoke test
//
// State is kept in process-local k/v storage so we never mix Int + Call or
// Int + Ident in `+` (which the codegen heuristic emits as string concat
// on tagged-pointer values, segfaulting on Int operands).
fn fail(label: String, got: String, want: String) -> Int {
println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
state_set("failures", "1")
0
}
fn check_eq(label: String, got: String, want: String) -> Int {
if got == want {
println("ok " + label + " = " + got)
return 1
}
fail(label, got, want)
}
check_eq("sanitize crates/nc-core",
sanitize_id("crates/nc-core"), "crates_nc_core")
check_eq("sanitize package.json",
sanitize_id("package.json"), "package_json")
check_eq("sanitize 42-module",
sanitize_id("42-module"), "n42_module")
check_eq("sanitize empty", sanitize_id(""), "node")
check_eq("sanitize !!--@@", sanitize_id("!!--@@"), "node")
check_eq("shape_from_token rounded",
shape_from_token("rounded"), "rounded")
check_eq("shape_to_node_shape primary",
shape_to_node_shape("primary"), "rectangle")
// Lowering preserves a node id and adds style.
let n: Map<String, Any> = make_node("svc", "Service", "primary")
let ln: Map<String, Any> = lower_node(n)
check_eq("lower preserves id", ln["id"], "svc")
check_eq("lower applies primary fill", ln["style_fill"], "#0052A0")
// Edge lowering
let e: Map<String, Any> = make_edge("a", "b", "dashed")
let le: Map<String, Any> = lower_edge(e)
check_eq("lower edge dashed line", le["line"], "dashed")
let e2: Map<String, Any> = make_edge("a", "b", "bidirectional")
let le2: Map<String, Any> = lower_edge(e2)
check_eq("lower edge bidirectional arrow", le2["arrow"], "both")
println("")
let failures: String = state_get("failures")
if str_eq(failures, "1") {
println("arbor-core: FAILED")
exit_program(1)
} else {
println("arbor-core: ok")
}
-19
View File
@@ -1,19 +0,0 @@
// arbor-diagram diagram intermediate representation + Mermaid serializer
// + dependency-graph builders. Consumes raw graph values built by arbor-core
// or arbor-parse and produces Mermaid markup or other serializations.
vessel "arbor-diagram" {
version "0.1.0"
description "Diagram IR + Mermaid serializer + architecture diagram builders"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
-433
View File
@@ -1,433 +0,0 @@
// arbor-diagram diagram intermediate representation (AST + IR).
//
// Where arbor-core supplies the *.arbor source-language model Mermaid-safe
// IDs, ArborShape strings, ArborEdgeKind strings, and the lowered "diagram-
// form" map arbor-diagram exposes the same lowered model as the canonical
// IR for downstream serializers (arbor-render and any future Mermaid-style
// emitter). The two vessels overlap by design: arbor-core is responsible for
// *naming* the schema; arbor-diagram is responsible for *building* values
// against it.
//
// The Rust crate ships small AST builder structs (`DiagramNode::new`,
// `DiagramEdge::with_label`, `DiagramGraph::add_node`). El has no method
// chaining, no Default::default(), no enum types. The El idiom is a stack
// of immutable maps with explicit constructor + with_* helpers that take
// the value and return a freshly-allocated map.
//
// Public surface:
// make_node(id, label) DiagramNode
// with_shape(node, shape) DiagramNode
// with_sublabel(node, sublabel) DiagramNode
// with_style(node, fill, stroke, color) DiagramNode
//
// make_edge(from, to) DiagramEdge
// with_label(edge, label)
// with_line(edge, line) // "solid"/"dashed"/"dotted"/"thick"
// with_arrow(edge, arrow) // "forward"/"backward"/"both"/"none"
//
// make_group(id, label) DiagramGroup
// with_node(group, node_id)
// with_nodes(group, [node_id])
// with_direction(group, dir)
//
// make_graph(title) DiagramGraph
// with_direction(graph, dir)
// graph_add_node(graph, node) DiagramGraph
// graph_add_edge(graph, edge) DiagramGraph
// graph_add_group(graph, group) DiagramGraph
// graph_node(graph, id) DiagramNode | empty map
//
// Shape vocabulary (lowered): see arbor-core. The local copy here mirrors
// the table in arbor-core/src/main.el so this vessel is hermetic.
// NodeShape vocabulary
fn node_shape_rectangle() -> String { "rectangle" }
fn node_shape_rounded_rect() -> String { "rounded_rect" }
fn node_shape_stadium() -> String { "stadium" }
fn node_shape_cylinder() -> String { "cylinder" }
fn node_shape_diamond() -> String { "diamond" }
fn node_shape_parallelogram() -> String { "parallelogram" }
fn node_shape_database() -> String { "database" }
fn node_shape_subroutine() -> String { "subroutine" }
fn node_shape_valid(s: String) -> Bool {
if str_eq(s, "rectangle") { return true }
if str_eq(s, "rounded_rect") { return true }
if str_eq(s, "stadium") { return true }
if str_eq(s, "cylinder") { return true }
if str_eq(s, "diamond") { return true }
if str_eq(s, "parallelogram") { return true }
if str_eq(s, "database") { return true }
if str_eq(s, "subroutine") { return true }
false
}
// EdgeLine vocabulary
fn edge_line_solid() -> String { "solid" }
fn edge_line_dashed() -> String { "dashed" }
fn edge_line_dotted() -> String { "dotted" }
fn edge_line_thick() -> String { "thick" }
fn edge_line_valid(s: String) -> Bool {
if str_eq(s, "solid") { return true }
if str_eq(s, "dashed") { return true }
if str_eq(s, "dotted") { return true }
if str_eq(s, "thick") { return true }
false
}
// EdgeArrow vocabulary
fn edge_arrow_forward() -> String { "forward" }
fn edge_arrow_backward() -> String { "backward" }
fn edge_arrow_both() -> String { "both" }
fn edge_arrow_none() -> String { "none" }
fn edge_arrow_valid(s: String) -> Bool {
if str_eq(s, "forward") { return true }
if str_eq(s, "backward") { return true }
if str_eq(s, "both") { return true }
if str_eq(s, "none") { return true }
false
}
// Direction vocabulary
fn direction_top_down() -> String { "top-down" }
fn direction_left_right() -> String { "left-right" }
fn direction_right_left() -> String { "right-left" }
fn direction_bottom_up() -> String { "bottom-up" }
fn direction_valid(s: String) -> Bool {
if str_eq(s, "top-down") { return true }
if str_eq(s, "left-right") { return true }
if str_eq(s, "right-left") { return true }
if str_eq(s, "bottom-up") { return true }
false
}
// DiagramNode
fn make_node(id: String, label: String) -> Map<String, Any> {
{
"id": id,
"label": label,
"sublabel": "",
"shape": "rectangle",
"style_fill": "",
"style_stroke": "",
"style_color": ""
}
}
fn with_shape(node: Map<String, Any>, shape: String) -> Map<String, Any> {
{
"id": node["id"],
"label": node["label"],
"sublabel": node["sublabel"],
"shape": shape,
"style_fill": node["style_fill"],
"style_stroke": node["style_stroke"],
"style_color": node["style_color"]
}
}
fn with_sublabel(node: Map<String, Any>, sublabel: String) -> Map<String, Any> {
{
"id": node["id"],
"label": node["label"],
"sublabel": sublabel,
"shape": node["shape"],
"style_fill": node["style_fill"],
"style_stroke": node["style_stroke"],
"style_color": node["style_color"]
}
}
fn with_style(node: Map<String, Any>, fill: String, stroke: String, color: String) -> Map<String, Any> {
{
"id": node["id"],
"label": node["label"],
"sublabel": node["sublabel"],
"shape": node["shape"],
"style_fill": fill,
"style_stroke": stroke,
"style_color": color
}
}
// DiagramEdge
fn make_edge(from: String, to: String) -> Map<String, Any> {
{
"from": from,
"to": to,
"label": "",
"line": "solid",
"arrow": "forward"
}
}
fn with_label(edge: Map<String, Any>, label: String) -> Map<String, Any> {
{
"from": edge["from"],
"to": edge["to"],
"label": label,
"line": edge["line"],
"arrow": edge["arrow"]
}
}
fn with_line(edge: Map<String, Any>, line: String) -> Map<String, Any> {
{
"from": edge["from"],
"to": edge["to"],
"label": edge["label"],
"line": line,
"arrow": edge["arrow"]
}
}
fn with_arrow(edge: Map<String, Any>, arrow: String) -> Map<String, Any> {
{
"from": edge["from"],
"to": edge["to"],
"label": edge["label"],
"line": edge["line"],
"arrow": arrow
}
}
// DiagramGroup
fn make_group(id: String, label: String) -> Map<String, Any> {
let empty: [String] = native_list_empty()
{
"id": id,
"label": label,
"node_ids": empty,
"direction": ""
}
}
fn with_node(group: Map<String, Any>, node_id: String) -> Map<String, Any> {
let cur: [String] = group["node_ids"]
let next: [String] = native_list_append(cur, node_id)
{
"id": group["id"],
"label": group["label"],
"node_ids": next,
"direction": group["direction"]
}
}
fn with_nodes(group: Map<String, Any>, ids: [String]) -> Map<String, Any> {
let cur: [String] = group["node_ids"]
let n: Int = el_list_len(ids)
let i = 0
while i < n {
let cur = native_list_append(cur, get(ids, i))
let i = i + 1
}
{
"id": group["id"],
"label": group["label"],
"node_ids": cur,
"direction": group["direction"]
}
}
fn with_group_direction(group: Map<String, Any>, dir: String) -> Map<String, Any> {
{
"id": group["id"],
"label": group["label"],
"node_ids": group["node_ids"],
"direction": dir
}
}
// DiagramGraph
fn make_graph(title: String) -> Map<String, Any> {
let empty_n: [Map<String, Any>] = native_list_empty()
let empty_e: [Map<String, Any>] = native_list_empty()
let empty_g: [Map<String, Any>] = native_list_empty()
{
"title": title,
"direction": "top-down",
"nodes": empty_n,
"edges": empty_e,
"groups": empty_g
}
}
fn with_direction(graph: Map<String, Any>, dir: String) -> Map<String, Any> {
{
"title": graph["title"],
"direction": dir,
"nodes": graph["nodes"],
"edges": graph["edges"],
"groups": graph["groups"]
}
}
fn graph_add_node(graph: Map<String, Any>, node: Map<String, Any>) -> Map<String, Any> {
let cur: [Map<String, Any>] = graph["nodes"]
let next: [Map<String, Any>] = native_list_append(cur, node)
{
"title": graph["title"],
"direction": graph["direction"],
"nodes": next,
"edges": graph["edges"],
"groups": graph["groups"]
}
}
fn graph_add_edge(graph: Map<String, Any>, edge: Map<String, Any>) -> Map<String, Any> {
let cur: [Map<String, Any>] = graph["edges"]
let next: [Map<String, Any>] = native_list_append(cur, edge)
{
"title": graph["title"],
"direction": graph["direction"],
"nodes": graph["nodes"],
"edges": next,
"groups": graph["groups"]
}
}
fn graph_add_group(graph: Map<String, Any>, group: Map<String, Any>) -> Map<String, Any> {
let cur: [Map<String, Any>] = graph["groups"]
let next: [Map<String, Any>] = native_list_append(cur, group)
{
"title": graph["title"],
"direction": graph["direction"],
"nodes": graph["nodes"],
"edges": graph["edges"],
"groups": next
}
}
// Find a node by id. Returns an empty map (no "id" field) when not present.
fn graph_node(graph: Map<String, Any>, id: String) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let n: Int = el_list_len(nodes)
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
if str_eq(nid, id) { return nd }
let i = i + 1
}
let empty: Map<String, Any> = el_map_new(0)
empty
}
// Smoke test
fn fail(label: String, got: String, want: String) -> Int {
println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
state_set("smoke_failures", "1")
0
}
fn check_eq(label: String, got: String, want: String) -> Int {
if got == want {
println("ok " + label + " = " + got)
return 1
}
fail(label, got, want)
}
// Vocabulary self-checks
check_eq("shape rectangle valid",
bool_to_str(node_shape_valid("rectangle")), "true")
check_eq("shape hexagon invalid",
bool_to_str(node_shape_valid("hexagon")), "false")
check_eq("line dashed valid",
bool_to_str(edge_line_valid("dashed")), "true")
check_eq("arrow both valid",
bool_to_str(edge_arrow_valid("both")), "true")
check_eq("dir top-down valid",
bool_to_str(direction_valid("top-down")), "true")
// Node builder
let n0: Map<String, Any> = make_node("svc", "Service")
check_eq("node default shape", n0["shape"], "rectangle")
check_eq("node default sublabel empty", n0["sublabel"], "")
let n1: Map<String, Any> = with_shape(n0, "cylinder")
check_eq("node with_shape", n1["shape"], "cylinder")
check_eq("node id preserved", n1["id"], "svc")
let n2: Map<String, Any> = with_sublabel(n1, "v0.1.0")
check_eq("node with_sublabel", n2["sublabel"], "v0.1.0")
let n3: Map<String, Any> = with_style(n2, "#0052A0", "#0052A0", "#ffffff")
check_eq("node style fill", n3["style_fill"], "#0052A0")
check_eq("node style color", n3["style_color"], "#ffffff")
// Edge builder
let e0: Map<String, Any> = make_edge("a", "b")
check_eq("edge default line", e0["line"], "solid")
check_eq("edge default arrow", e0["arrow"], "forward")
let e1: Map<String, Any> = with_line(e0, "dashed")
let e2: Map<String, Any> = with_arrow(e1, "both")
let e3: Map<String, Any> = with_label(e2, "calls")
check_eq("edge line", e3["line"], "dashed")
check_eq("edge arrow", e3["arrow"], "both")
check_eq("edge label", e3["label"], "calls")
// Group builder
let g0: Map<String, Any> = make_group("core", "Application Core")
let g1: Map<String, Any> = with_node(g0, "api")
let g2: Map<String, Any> = with_node(g1, "svc")
let ids2: [String] = g2["node_ids"]
check_eq("group with two nodes", int_to_str(el_list_len(ids2)), "2")
let g3: Map<String, Any> = make_group("infra", "Infrastructure")
let extras: [String] = native_list_empty()
let extras = native_list_append(extras, "db")
let extras = native_list_append(extras, "cache")
let g4: Map<String, Any> = with_nodes(g3, extras)
let ids4: [String] = g4["node_ids"]
check_eq("group with_nodes appends", int_to_str(el_list_len(ids4)), "2")
// Graph builder + lookup
let G0: Map<String, Any> = make_graph("System")
let G1: Map<String, Any> = with_direction(G0, "left-right")
let G2: Map<String, Any> = graph_add_node(G1, n3)
let nb: Map<String, Any> = make_node("b", "Backend")
let G3: Map<String, Any> = graph_add_node(G2, nb)
let G4: Map<String, Any> = graph_add_edge(G3, e3)
let G5: Map<String, Any> = graph_add_group(G4, g4)
check_eq("graph title", G5["title"], "System")
check_eq("graph direction", G5["direction"], "left-right")
let gn: [Map<String, Any>] = G5["nodes"]
let ge: [Map<String, Any>] = G5["edges"]
let gg: [Map<String, Any>] = G5["groups"]
check_eq("graph nodes count", int_to_str(el_list_len(gn)), "2")
check_eq("graph edges count", int_to_str(el_list_len(ge)), "1")
check_eq("graph groups count", int_to_str(el_list_len(gg)), "1")
let found: Map<String, Any> = graph_node(G5, "svc")
check_eq("graph_node found", found["id"], "svc")
let missing: Map<String, Any> = graph_node(G5, "nonexistent")
let missing_id: String = missing["id"]
if str_len(missing_id) == 0 {
println("ok graph_node missing returns empty")
} else {
println("FAIL graph_node missing returned: " + missing_id)
state_set("smoke_failures", "1")
}
println("")
let failures: String = state_get("smoke_failures")
if str_eq(failures, "1") {
println("arbor-diagram: FAILED")
exit_program(1)
} else {
println("arbor-diagram: ok")
}
-19
View File
@@ -1,19 +0,0 @@
// arbor-layout hierarchical layout engine. Assigns (x, y) positions to
// every node, computes group bounding boxes, and the canvas size. Consumes
// a diagram graph; produces a layout-result value.
vessel "arbor-layout" {
version "0.1.0"
description "Hierarchical layout engine — rank assignment, positioning, group bounds"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
-591
View File
@@ -1,591 +0,0 @@
// arbor-layout hierarchical layout for diagram graphs.
//
// Public entry point:
// fn arbor_layout(graph: Map<String, Any>) -> Map<String, Any>
//
// The graph is the lowered (diagram-form) shape. The result map has:
// "node_pos_<id>" { "x":Float, "y":Float } centre point
// "node_size_<id>" { "w":Float, "h":Float }
// "group_bounds_<id>" { "x":Float, "y":Float, "w":Float, "h":Float }
// "node_ids" [String] iteration order
// "group_ids" [String] iteration order
// "canvas" { "w":Float, "h":Float }
//
// Floats are El-encoded store via the runtime's bit-cast convention.
// All arithmetic on positions/sizes is done in Float; integers (rank index)
// stay as Int.
//
// Algorithm (simplified Sugiyama):
// 1. Assign ranks via topological propagation (longest path from sources).
// 2. Group nodes by rank, preserving declaration order.
// 3. Position each rank as a row (top-down/bottom-up) or column (LR/RL).
// 4. Compute group bounding boxes from member positions.
// 5. Compute canvas size to enclose everything.
//
// The current implementation is the same simplified Sugiyama as the Rust
// version; perfectly identical numerical output is not promised but the
// relative ordering and bounding-box semantics match.
// Spacing constants (declared as float-bit-cast helpers)
fn k_node_base_w() -> el_val_t { int_to_float(120) }
fn k_node_base_h() -> el_val_t { int_to_float(40) }
fn k_node_char_extra() -> el_val_t { int_to_float(8) }
fn k_h_gap() -> el_val_t { int_to_float(60) }
fn k_v_gap() -> el_val_t { int_to_float(80) }
fn k_group_pad() -> el_val_t { int_to_float(20) }
fn k_margin() -> el_val_t { int_to_float(40) }
// Float-aware max/min via int_to_float / float arithmetic but el_max
// works in raw int comparison space, so we bit-cast carefully.
// For our purposes we only need monotonic comparisons on positive values,
// which IEEE 754 doubles + sign-magnitude bit patterns happen to preserve
// for non-negative floats but it's safer to do the comparison via the
// math layer. We use a helper that decodes both, picks the bigger, and
// re-encodes.
//
// Implemented in C terms: math_max(a, b) but el_runtime doesn't expose
// a float-aware max, so we synthesise one.
fn fmax(a: el_val_t, b: el_val_t) -> el_val_t {
// Compare via float subtraction's sign: a - b. Float subtraction is the
// multiply chain implemented via the C code generator. But el's `-` on
// bit-cast doubles doesn't perform IEEE arithmetic it's a 64-bit int
// subtract. Workaround: round-trip through format_float and str_to_float.
// For our layout numbers (small non-negative integers stored as floats)
// we can compare via the raw bits: a positive float's bit pattern is
// monotonically ordered, so `a > b` on the int reinterpretation gives
// the same result as on the actual double for non-negative values.
if a > b { return a }
b
}
fn fadd(a: el_val_t, b: el_val_t) -> el_val_t {
// a, b are bit-cast doubles. Safe addition: int-to-float, format, parse.
// For the small positive integers we work with, we reconstruct the
// numeric value via format_float str_to_float, perform addition by
// pulling them through str representations. Costly but correct on the
// current runtime. Fast path: if both are exact ints stored as floats
// we can also keep an Int "shadow" but the simpler approach is to
// route through the printf-based formatter once per layout pass.
let as: String = format_float(a, 6)
let bs: String = format_float(b, 6)
// Parse back to numeric.
let af: el_val_t = str_to_float(as)
let bf: el_val_t = str_to_float(bs)
// No real-add primitive; build the sum from int parts where possible.
// Convert to int at full resolution: float_to_int truncates towards zero,
// which for our values (always integer-valued) is exact.
let ai: Int = float_to_int(af)
let bi: Int = float_to_int(bf)
int_to_float(ai + bi)
}
fn fsub(a: el_val_t, b: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
let bi: Int = float_to_int(b)
int_to_float(ai - bi)
}
fn fmul(a: el_val_t, b: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
let bi: Int = float_to_int(b)
int_to_float(ai * bi)
}
fn fdiv2(a: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
int_to_float(ai / 2)
}
// Node size based on label width
fn node_size_for(label: String) -> Map<String, Any> {
let len: Int = str_len(label)
let extra: Int = 0
if len > 10 {
let extra = len - 10
}
let w_int: Int = 120 + 8 * extra
let w: el_val_t = int_to_float(w_int)
let h: el_val_t = int_to_float(40)
{ "w": w, "h": h }
}
// Adjacency-list construction
//
// Builds successor and in-degree maps keyed by node id.
fn build_succ_indeg(graph: Map<String, Any>) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let edges: [Map<String, Any>] = graph["edges"]
let n: Int = el_list_len(nodes)
let m: Int = el_list_len(edges)
let succ: Map<String, Any> = el_map_new(0)
let indeg: Map<String, Any> = el_map_new(0)
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let empty: [String] = el_list_empty()
let succ = el_map_set(succ, nid, empty)
let indeg = el_map_set(indeg, nid, 0)
let i = i + 1
}
let i = 0
while i < m {
let e: Map<String, Any> = get(edges, i)
let src: String = e["from"]
let dst: String = e["to"]
let cur_succ: [String] = el_map_get(succ, src)
let new_succ: [String] = native_list_append(cur_succ, dst)
let succ = el_map_set(succ, src, new_succ)
let prev: Int = el_map_get(indeg, dst)
let indeg = el_map_set(indeg, dst, prev + 1)
let i = i + 1
}
{ "succ": succ, "indeg": indeg }
}
// Topological rank assignment
//
// Returns a map: node_id rank.
fn assign_ranks(graph: Map<String, Any>) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let n: Int = el_list_len(nodes)
let adj: Map<String, Any> = build_succ_indeg(graph)
let succ: Map<String, Any> = adj["succ"]
let indeg: Map<String, Any> = adj["indeg"]
let ranks: Map<String, Any> = el_map_new(0)
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let ranks = el_map_set(ranks, nid, 0)
let i = i + 1
}
// Initialise queue with all nodes whose in-degree is 0 (in declaration
// order, mirroring the Rust implementation's ordering guarantee).
let queue: [String] = el_list_empty()
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let d: Int = el_map_get(indeg, nid)
if d == 0 {
let queue = native_list_append(queue, nid)
}
let i = i + 1
}
let head = 0
let running = true
while running {
if head >= el_list_len(queue) {
let running = false
} else {
let cur: String = get(queue, head)
let head = head + 1
let cur_rank: Int = el_map_get(ranks, cur)
let neighbours: [String] = el_map_get(succ, cur)
let nn: Int = el_list_len(neighbours)
let j = 0
while j < nn {
let nb: String = get(neighbours, j)
let nb_rank: Int = el_map_get(ranks, nb)
let cand: Int = cur_rank + 1
if cand > nb_rank {
let ranks = el_map_set(ranks, nb, cand)
}
let cur_d: Int = el_map_get(indeg, nb)
let new_d: Int = cur_d - 1
let indeg = el_map_set(indeg, nb, new_d)
if new_d <= 0 {
let queue = native_list_append(queue, nb)
}
let j = j + 1
}
}
}
ranks
}
// Layout pass
fn arbor_layout(graph: Map<String, Any>) -> Map<String, Any> {
let nodes: [Map<String, Any>] = graph["nodes"]
let n: Int = el_list_len(nodes)
let direction: String = graph["direction"]
let result: Map<String, Any> = el_map_new(0)
let result = el_map_set(result, "node_ids", el_list_empty())
let result = el_map_set(result, "group_ids", el_list_empty())
if n == 0 {
let canvas: Map<String, Any> = { "w": int_to_float(200), "h": int_to_float(100) }
let result = el_map_set(result, "canvas", canvas)
return result
}
let ranks: Map<String, Any> = assign_ranks(graph)
let max_rank = 0
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let r: Int = el_map_get(ranks, nid)
if r > max_rank { let max_rank = r }
let i = i + 1
}
// Group nodes by rank, preserving declaration order. Buckets are stored
// in process state so we can iterate without nested-list mutation.
let i = 0
while i <= max_rank {
state_set("rank_bucket_" + int_to_str(i), "")
let i = i + 1
}
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let r: Int = el_map_get(ranks, nid)
let key = "rank_bucket_" + int_to_str(r)
let prev: String = state_get(key)
if str_eq(prev, "") {
state_set(key, nid)
} else {
state_set(key, prev + "" + nid)
}
let i = i + 1
}
// Pre-compute sizes and stash a label-keyed cache.
let id_list: [String] = el_list_empty()
let i = 0
while i < n {
let nd: Map<String, Any> = get(nodes, i)
let nid: String = nd["id"]
let lbl: String = nd["label"]
let sz: Map<String, Any> = node_size_for(lbl)
let result = el_map_set(result, "node_size_" + nid, sz)
let id_list = native_list_append(id_list, nid)
let i = i + 1
}
let result = el_map_set(result, "node_ids", id_list)
// Position pass.
let is_vertical = true
if str_eq(direction, "left-right") { let is_vertical = false }
if str_eq(direction, "right-left") { let is_vertical = false }
let cursor: el_val_t = k_margin()
let r = 0
while r <= max_rank {
let bucket_str: String = state_get("rank_bucket_" + int_to_str(r))
if !str_eq(bucket_str, "") {
let ids: [String] = str_split(bucket_str, "")
let ids_n: Int = el_list_len(ids)
// Track row height (for vertical) or column width (for horizontal).
let cross_max: el_val_t = int_to_float(40)
let j = 0
while j < ids_n {
let nid: String = get(ids, j)
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
if is_vertical {
let h: el_val_t = sz["h"]
let cross_max = fmax(cross_max, h)
} else {
let w: el_val_t = sz["w"]
let cross_max = fmax(cross_max, w)
}
let j = j + 1
}
if is_vertical {
let row_h: el_val_t = cross_max
let y_center: el_val_t = fadd(cursor, fdiv2(row_h))
let x_cursor: el_val_t = k_margin()
let j = 0
while j < ids_n {
let nid: String = get(ids, j)
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
let w: el_val_t = sz["w"]
let cx: el_val_t = fadd(x_cursor, fdiv2(w))
let pos: Map<String, Any> = { "x": cx, "y": y_center }
let result = el_map_set(result, "node_pos_" + nid, pos)
let x_cursor = fadd(fadd(x_cursor, w), k_h_gap())
let j = j + 1
}
let cursor = fadd(fadd(cursor, row_h), k_v_gap())
} else {
let col_w: el_val_t = cross_max
let x_center: el_val_t = fadd(cursor, fdiv2(col_w))
let y_cursor: el_val_t = k_margin()
let j = 0
while j < ids_n {
let nid: String = get(ids, j)
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
let h: el_val_t = sz["h"]
let cy: el_val_t = fadd(y_cursor, fdiv2(h))
let pos: Map<String, Any> = { "x": x_center, "y": cy }
let result = el_map_set(result, "node_pos_" + nid, pos)
let y_cursor = fadd(fadd(y_cursor, h), k_v_gap())
let j = j + 1
}
let cursor = fadd(fadd(cursor, col_w), k_h_gap())
}
} else {
// Empty bucket advance cursor by a default node size.
if is_vertical {
let cursor = fadd(cursor, fadd(int_to_float(40), k_v_gap()))
} else {
let cursor = fadd(cursor, fadd(k_node_base_w(), k_h_gap()))
}
}
let r = r + 1
}
// Direction inversions for BU / RL.
let need_flip_y = false
let need_flip_x = false
if str_eq(direction, "bottom-up") { let need_flip_y = true }
if str_eq(direction, "right-left") { let need_flip_x = true }
if need_flip_y {
let max_y: el_val_t = fadd(fsub(cursor, k_v_gap()), k_margin())
let i = 0
while i < n {
let nid: String = get(id_list, i)
let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
let y: el_val_t = pos["y"]
let new_y: el_val_t = fadd(fsub(max_y, y), k_margin())
let new_pos: Map<String, Any> = { "x": pos["x"], "y": new_y }
let result = el_map_set(result, "node_pos_" + nid, new_pos)
let i = i + 1
}
}
if need_flip_x {
let max_x: el_val_t = fadd(fsub(cursor, k_h_gap()), k_margin())
let i = 0
while i < n {
let nid: String = get(id_list, i)
let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
let x: el_val_t = pos["x"]
let new_x: el_val_t = fadd(fsub(max_x, x), k_margin())
let new_pos: Map<String, Any> = { "x": new_x, "y": pos["y"] }
let result = el_map_set(result, "node_pos_" + nid, new_pos)
let i = i + 1
}
}
// Group bounds.
let groups: [Map<String, Any>] = graph["groups"]
let gn: Int = el_list_len(groups)
let gid_list: [String] = el_list_empty()
let g = 0
while g < gn {
let grp: Map<String, Any> = get(groups, g)
let gid: String = grp["id"]
let member_ids: [String] = grp["node_ids"]
let mn: Int = el_list_len(member_ids)
if mn > 0 {
let big: Int = 1000000000
let neg: Int = 0 - 1000000000
let min_x: el_val_t = int_to_float(big)
let min_y: el_val_t = int_to_float(big)
let max_x: el_val_t = int_to_float(neg)
let max_y: el_val_t = int_to_float(neg)
let mi = 0
while mi < mn {
let mid: String = get(member_ids, mi)
let mpos: Map<String, Any> = el_map_get(result, "node_pos_" + mid)
let msz: Map<String, Any> = el_map_get(result, "node_size_" + mid)
let mid_present: String = mpos["x"]
if str_len(mid_present) >= 0 {
let cx: el_val_t = mpos["x"]
let cy: el_val_t = mpos["y"]
let mw: el_val_t = msz["w"]
let mh: el_val_t = msz["h"]
let left: el_val_t = fsub(cx, fdiv2(mw))
let right: el_val_t = fadd(cx, fdiv2(mw))
let top: el_val_t = fsub(cy, fdiv2(mh))
let bot: el_val_t = fadd(cy, fdiv2(mh))
if left < min_x { let min_x = left }
if top < min_y { let min_y = top }
if right > max_x { let max_x = right }
if bot > max_y { let max_y = bot }
}
let mi = mi + 1
}
let bx: el_val_t = fsub(min_x, k_group_pad())
let by: el_val_t = fsub(min_y, k_group_pad())
let bw: el_val_t = fadd(fsub(max_x, min_x), fmul(k_group_pad(), int_to_float(2)))
let bh: el_val_t = fadd(fsub(max_y, min_y), fmul(k_group_pad(), int_to_float(2)))
let bounds: Map<String, Any> = { "x": bx, "y": by, "w": bw, "h": bh }
let result = el_map_set(result, "group_bounds_" + gid, bounds)
let gid_list = native_list_append(gid_list, gid)
}
let g = g + 1
}
let result = el_map_set(result, "group_ids", gid_list)
// Canvas size = max node-right / node-bottom + group-right / group-bottom.
let canvas_w: el_val_t = int_to_float(0)
let canvas_h: el_val_t = int_to_float(0)
let i = 0
while i < n {
let nid: String = get(id_list, i)
let pos: Map<String, Any> = el_map_get(result, "node_pos_" + nid)
let sz: Map<String, Any> = el_map_get(result, "node_size_" + nid)
let right: el_val_t = fadd(pos["x"], fdiv2(sz["w"]))
let bottom: el_val_t = fadd(pos["y"], fdiv2(sz["h"]))
if right > canvas_w { let canvas_w = right }
if bottom > canvas_h { let canvas_h = bottom }
let i = i + 1
}
let i = 0
while i < el_list_len(gid_list) {
let gid: String = get(gid_list, i)
let b: Map<String, Any> = el_map_get(result, "group_bounds_" + gid)
let r: el_val_t = fadd(b["x"], b["w"])
let bt: el_val_t = fadd(b["y"], b["h"])
if r > canvas_w { let canvas_w = r }
if bt > canvas_h { let canvas_h = bt }
let i = i + 1
}
let canvas: Map<String, Any> = {
"w": fadd(canvas_w, k_margin()),
"h": fadd(canvas_h, k_margin())
}
let result = el_map_set(result, "canvas", canvas)
result
}
// Smoke test
fn fl_to_str(v: el_val_t) -> String {
int_to_str(float_to_int(v))
}
fn smoke_fail(label: String, msg: String) -> Int {
println("FAIL " + label + ": " + msg)
state_set("smoke_failures", "1")
0
}
fn make_test_node(id: String, label: String) -> Map<String, Any> {
{
"id": id, "label": label, "sublabel": "",
"shape": "rectangle",
"style_fill": "", "style_stroke": "", "style_color": ""
}
}
fn make_test_edge(src: String, dst: String) -> Map<String, Any> {
{ "from": src, "to": dst, "label": "", "line": "solid", "arrow": "forward" }
}
fn make_test_graph(direction: String, ids: [String], src_dst: [String]) -> Map<String, Any> {
let nodes: [Map<String, Any>] = el_list_empty()
let i = 0
while i < el_list_len(ids) {
let nid: String = get(ids, i)
let nodes = native_list_append(nodes, make_test_node(nid, nid))
let i = i + 1
}
let edges: [Map<String, Any>] = el_list_empty()
let i = 0
while i + 1 < el_list_len(src_dst) {
let s: String = get(src_dst, i)
let d: String = get(src_dst, i + 1)
let edges = native_list_append(edges, make_test_edge(s, d))
let i = i + 2
}
{
"title": "T", "direction": direction,
"nodes": nodes, "edges": edges, "groups": el_list_empty()
}
}
// Empty graph.
let g_empty: Map<String, Any> = {
"title": "e", "direction": "top-down",
"nodes": el_list_empty(), "edges": el_list_empty(), "groups": el_list_empty()
}
let r_empty: Map<String, Any> = arbor_layout(g_empty)
let canvas_empty: Map<String, Any> = r_empty["canvas"]
println("empty canvas w=" + fl_to_str(canvas_empty["w"]))
// Single node.
let g_one: Map<String, Any> = make_test_graph("top-down",
["solo"], el_list_empty())
let r_one: Map<String, Any> = arbor_layout(g_one)
let pos_solo: Map<String, Any> = el_map_get(r_one, "node_pos_solo")
let x_solo: el_val_t = pos_solo["x"]
let y_solo: el_val_t = pos_solo["y"]
println("solo at x=" + fl_to_str(x_solo) + " y=" + fl_to_str(y_solo))
if float_to_int(x_solo) <= 0 { smoke_fail("solo x", "expected > 0") }
if float_to_int(y_solo) <= 0 { smoke_fail("solo y", "expected > 0") }
// Linear chain abc top-down: ya < yb < yc.
let g_chain: Map<String, Any> = make_test_graph("top-down",
["a", "b", "c"], ["a", "b", "b", "c"])
let r_chain: Map<String, Any> = arbor_layout(g_chain)
let pa: Map<String, Any> = el_map_get(r_chain, "node_pos_a")
let pb: Map<String, Any> = el_map_get(r_chain, "node_pos_b")
let pc: Map<String, Any> = el_map_get(r_chain, "node_pos_c")
let ya: el_val_t = pa["y"]
let yb: el_val_t = pb["y"]
let yc: el_val_t = pc["y"]
println("td a.y=" + fl_to_str(ya) + " b.y=" + fl_to_str(yb) + " c.y=" + fl_to_str(yc))
if float_to_int(ya) >= float_to_int(yb) { smoke_fail("td order", "a.y >= b.y") }
if float_to_int(yb) >= float_to_int(yc) { smoke_fail("td order", "b.y >= c.y") }
// LR direction
let g_lr: Map<String, Any> = make_test_graph("left-right",
["a", "b", "c"], ["a", "b", "b", "c"])
let r_lr: Map<String, Any> = arbor_layout(g_lr)
let pa2: Map<String, Any> = el_map_get(r_lr, "node_pos_a")
let pc2: Map<String, Any> = el_map_get(r_lr, "node_pos_c")
let xa: el_val_t = pa2["x"]
let xc: el_val_t = pc2["x"]
println("lr a.x=" + fl_to_str(xa) + " c.x=" + fl_to_str(xc))
if float_to_int(xa) >= float_to_int(xc) { smoke_fail("lr order", "a.x >= c.x") }
// Bottom-up: a is below c.
let g_bu: Map<String, Any> = make_test_graph("bottom-up",
["a", "b", "c"], ["a", "b", "b", "c"])
let r_bu: Map<String, Any> = arbor_layout(g_bu)
let pa3: Map<String, Any> = el_map_get(r_bu, "node_pos_a")
let pc3: Map<String, Any> = el_map_get(r_bu, "node_pos_c")
let ya3: el_val_t = pa3["y"]
let yc3: el_val_t = pc3["y"]
println("bu a.y=" + fl_to_str(ya3) + " c.y=" + fl_to_str(yc3))
if float_to_int(ya3) <= float_to_int(yc3) { smoke_fail("bu order", "a.y <= c.y") }
// Canvas covers all nodes.
let canvas_chain: Map<String, Any> = r_chain["canvas"]
let cw: el_val_t = canvas_chain["w"]
let ch: el_val_t = canvas_chain["h"]
println("chain canvas w=" + fl_to_str(cw) + " h=" + fl_to_str(ch))
if float_to_int(cw) <= 0 { smoke_fail("canvas w", "non-positive") }
if float_to_int(ch) <= 0 { smoke_fail("canvas h", "non-positive") }
println("")
let f: String = state_get("smoke_failures")
if str_eq(f, "1") {
println("arbor-layout: FAILED")
exit_program(1)
} else {
println("arbor-layout: ok")
}
-19
View File
@@ -1,19 +0,0 @@
// arbor-parse hand-written recursive-descent parser for the .arbor source
// language. Produces an Arbor graph value consumable by arbor-layout and
// arbor-render.
vessel "arbor-parse" {
version "0.1.0"
description "Recursive-descent parser for the .arbor diagram language"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
-763
View File
@@ -1,763 +0,0 @@
// arbor-parse recursive-descent parser for the .arbor source language.
//
// This vessel inlines a private copy of the small set of arbor-core helpers
// it needs (sanitize_id and constructors). El's import form today is purely
// syntactic concatenation, so each vessel that wants to be its own buildable
// unit carries its own copy of these helpers. They're tiny (well under 100
// lines) and the duplication keeps each vessel hermetic.
//
// Public entry point: fn arbor_parse(source: String) -> Map<String, Any>
//
// Returns either a graph value or a parse-error map. Callers test for the
// "error" field:
// { "error": "..." , "line": Int, "text": "...source line..." } on failure
// { "title", "direction", "nodes", "edges", "groups" } on success
// Sanitisation (copy of arbor-core's sanitize_id)
fn is_alnum_underscore(ch: String) -> Bool {
let code: Int = str_char_code(ch, 0)
if code >= 48 {
if code <= 57 { return true }
}
if code >= 65 {
if code <= 90 { return true }
}
if code >= 97 {
if code <= 122 { return true }
}
if code == 95 { return true }
false
}
fn is_ascii_digit(ch: String) -> Bool {
let code: Int = str_char_code(ch, 0)
if code >= 48 {
if code <= 57 { return true }
}
false
}
fn sanitize_id(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "node" }
let out = ""
let prev_underscore = false
let i = 0
while i < n {
let ch: String = str_char_at(s, i)
if is_alnum_underscore(ch) {
let out = out + ch
let prev_underscore = false
} else {
if !prev_underscore {
let out = out + "_"
}
let prev_underscore = true
}
let i = i + 1
}
let m: Int = str_len(out)
let end = m
let stripping = true
while stripping {
if end <= 0 {
let stripping = false
} else {
let last: String = str_char_at(out, end - 1)
if last == "_" {
let end = end - 1
} else {
let stripping = false
}
}
}
let out = str_slice(out, 0, end)
if str_len(out) == 0 { return "node" }
let first: String = str_char_at(out, 0)
if is_ascii_digit(first) {
let out = "n" + out
}
out
}
fn shape_from_token(tok: String) -> String {
let t: String = str_trim(tok)
if t == "rect" { return "rect" }
if t == "rounded" { return "rounded" }
if t == "cylinder" { return "cylinder" }
if t == "diamond" { return "diamond" }
if t == "stadium" { return "stadium" }
if t == "primary" { return "primary" }
""
}
// Line preprocessing
//
// Strip inline `// ...` comments, trim, drop empties. Returns a list of maps
// { "no": Int, "text": String }.
fn preprocess(source: String) -> [Map<String, Any>] {
let lines: [String] = str_split(source, "\n")
let n: Int = el_list_len(lines)
let out: [Map<String, Any>] = el_list_empty()
let i = 0
while i < n {
let raw: String = get(lines, i)
let cidx: Int = str_index_of(raw, "//")
let stripped = raw
if cidx >= 0 {
let stripped = str_slice(raw, 0, cidx)
}
let trimmed: String = str_trim(stripped)
if str_len(trimmed) > 0 {
let row: Map<String, Any> = { "no": i + 1, "text": trimmed }
let out = native_list_append(out, row)
}
let i = i + 1
}
out
}
// Quoted-string extraction
//
// Parses `"text"`-prefix from a string. Returns `{ "ok": Bool, "value": Str,
// "rest": Str }`. The `rest` field carries everything after the closing quote
// (so the caller can continue tokenising).
fn parse_quoted(s: String) -> Map<String, Any> {
let t: String = str_trim(s)
if str_len(t) < 2 {
return { "ok": false, "value": "", "rest": s }
}
let first: String = str_char_at(t, 0)
if first != "\"" {
return { "ok": false, "value": "", "rest": s }
}
let body: String = str_slice(t, 1, str_len(t))
let close: Int = str_index_of(body, "\"")
if close < 0 {
return { "ok": false, "value": "", "rest": s }
}
let inner: String = str_slice(body, 0, close)
let rest: String = str_slice(body, close + 1, str_len(body))
{ "ok": true, "value": inner, "rest": rest }
}
// Identifier prefix split
//
// `split_identifier("foo bar")` { "id": "foo", "rest": " bar" }.
// `split_identifier("a-b")` { "id": "a", "rest": "-b" }.
fn split_identifier(s: String) -> Map<String, Any> {
let n: Int = str_len(s)
let i = 0
while i < n {
let ch: String = str_char_at(s, i)
if !is_alnum_underscore(ch) {
return { "id": str_slice(s, 0, i), "rest": str_slice(s, i, n) }
}
let i = i + 1
}
{ "id": s, "rest": "" }
}
// Direction parsing
fn parse_direction(s: String) -> String {
let t: String = str_trim(s)
if t == "top-down" { return "top-down" }
if t == "TD" { return "top-down" }
if t == "left-right" { return "left-right" }
if t == "LR" { return "left-right" }
if t == "right-left" { return "right-left" }
if t == "RL" { return "right-left" }
if t == "bottom-up" { return "bottom-up" }
if t == "BU" { return "bottom-up" }
""
}
// Edge-arrow detection
//
// Detects the longest matching arrow token in a line, returning
// { "ok": Bool, "from_str": Str, "kind": Str, "rest": Str }
fn extract_edge_parts(line: String) -> Map<String, Any> {
// Order: longest first to avoid partial matches.
let f1: Int = str_index_of(line, "-/->")
if f1 >= 0 {
return { "ok": true,
"from_str": str_slice(line, 0, f1),
"kind": "forbidden",
"rest": str_slice(line, f1 + 4, str_len(line)) }
}
let f2: Int = str_index_of(line, "<->")
if f2 >= 0 {
return { "ok": true,
"from_str": str_slice(line, 0, f2),
"kind": "bidirectional",
"rest": str_slice(line, f2 + 3, str_len(line)) }
}
let f3: Int = str_index_of(line, "-->")
if f3 >= 0 {
return { "ok": true,
"from_str": str_slice(line, 0, f3),
"kind": "dashed",
"rest": str_slice(line, f3 + 3, str_len(line)) }
}
let f4: Int = str_index_of(line, "->")
if f4 >= 0 {
return { "ok": true,
"from_str": str_slice(line, 0, f4),
"kind": "solid",
"rest": str_slice(line, f4 + 2, str_len(line)) }
}
{ "ok": false, "from_str": "", "kind": "", "rest": "" }
}
fn is_edge_line(line: String) -> Bool {
if str_contains(line, "->") { return true }
if str_contains(line, "<->") { return true }
false
}
// Error helpers
fn make_error(line_no: Int, line_text: String, message: String) -> Map<String, Any> {
{ "error": message, "line": line_no, "text": line_text }
}
// Parse driver
//
// State is held in process-local k/v rather than threaded through every
// function. Specifically:
// "title", "direction" graph header
// "nodes_json", "edges_json", "groups_json" accumulators (string lists)
// "group_stack_depth" "0".."N" open groups
// "group_stack_<i>_id" / "_label" / "_line" frame data
// "group_stack_<i>_node_ids" JSON array of ids inside frame
// "error" non-empty if parse failed
// "error_line", "error_text" context
fn st_set_int(key: String, v: Int) -> Int { state_set(key, int_to_str(v)); 0 }
fn st_get_int(key: String) -> Int {
let s: String = state_get(key)
if str_eq(s, "") { return 0 }
str_to_int(s)
}
// Encode/decode small string lists via "" delimiter (unit separator).
fn list_encode(xs: [String]) -> String {
let n: Int = el_list_len(xs)
let out = ""
let i = 0
while i < n {
if i > 0 { let out = out + "" }
let out = out + get(xs, i)
let i = i + 1
}
out
}
fn list_decode(s: String) -> [String] {
if str_eq(s, "") { return el_list_empty() }
str_split(s, "")
}
fn current_group_index() -> Int {
st_get_int("group_stack_depth") - 1
}
fn group_frame_key(idx: Int, suffix: String) -> String {
"gs_" + int_to_str(idx) + "_" + suffix
}
fn open_group(id: String, label: String, line_no: Int) -> Int {
let depth: Int = st_get_int("group_stack_depth")
state_set(group_frame_key(depth, "id"), id)
state_set(group_frame_key(depth, "label"), label)
state_set(group_frame_key(depth, "line"), int_to_str(line_no))
state_set(group_frame_key(depth, "ids"), "")
st_set_int("group_stack_depth", depth + 1)
0
}
fn close_group_frame() -> Map<String, Any> {
let depth: Int = st_get_int("group_stack_depth")
if depth <= 0 {
return { "ok": false, "id": "", "label": "", "ids": "" }
}
let idx: Int = depth - 1
let id: String = state_get(group_frame_key(idx, "id"))
let label: String = state_get(group_frame_key(idx, "label"))
let ids: String = state_get(group_frame_key(idx, "ids"))
state_del(group_frame_key(idx, "id"))
state_del(group_frame_key(idx, "label"))
state_del(group_frame_key(idx, "line"))
state_del(group_frame_key(idx, "ids"))
st_set_int("group_stack_depth", idx)
{ "ok": true, "id": id, "label": label, "ids": ids }
}
fn register_node_in_group(node_id: String) -> Int {
let depth: Int = st_get_int("group_stack_depth")
if depth <= 0 { return 0 }
let idx: Int = depth - 1
let key: String = group_frame_key(idx, "ids")
let prev: String = state_get(key)
if str_eq(prev, "") {
state_set(key, node_id)
} else {
state_set(key, prev + "" + node_id)
}
0
}
// Accumulator JSON-ish encoding for nodes/edges/groups.
// We render each entry as a small string and stash in state under a counter.
fn store_node(id: String, label: String, shape: String) -> Int {
let n: Int = st_get_int("node_count")
state_set("node_id_" + int_to_str(n), id)
state_set("node_label_" + int_to_str(n), label)
state_set("node_shape_" + int_to_str(n), shape)
st_set_int("node_count", n + 1)
0
}
fn store_edge(src: String, dst: String, label: String, kind: String) -> Int {
let n: Int = st_get_int("edge_count")
state_set("edge_from_" + int_to_str(n), src)
state_set("edge_to_" + int_to_str(n), dst)
state_set("edge_label_" + int_to_str(n), label)
state_set("edge_kind_" + int_to_str(n), kind)
st_set_int("edge_count", n + 1)
0
}
fn store_group(id: String, label: String, ids: String) -> Int {
let n: Int = st_get_int("group_count")
state_set("group_id_" + int_to_str(n), id)
state_set("group_label_" + int_to_str(n), label)
state_set("group_ids_" + int_to_str(n), ids)
st_set_int("group_count", n + 1)
0
}
fn set_error(msg: String, line_no: Int, line_text: String) -> Int {
state_set("parse_error", msg)
st_set_int("parse_error_line", line_no)
state_set("parse_error_text", line_text)
0
}
fn has_error() -> Bool {
let m: String = state_get("parse_error")
if str_eq(m, "") { return false }
true
}
// Reset state at the start of each parse pass.
fn reset_state() -> Int {
state_set("graph_title", "")
state_set("graph_direction", "top-down")
st_set_int("node_count", 0)
st_set_int("edge_count", 0)
st_set_int("group_count", 0)
st_set_int("group_stack_depth", 0)
state_set("parse_error", "")
st_set_int("parse_error_line", 0)
state_set("parse_error_text", "")
0
}
// Statement-level parsing
fn parse_node_stmt(line_no: Int, line: String) -> Int {
let id_split: Map<String, Any> = split_identifier(line)
let raw_id: String = id_split["id"]
if str_eq(raw_id, "") {
set_error("expected node id, edge, or keyword", line_no, line)
return 0
}
let id: String = sanitize_id(raw_id)
let rest: String = str_trim(id_split["rest"])
// Optional shape: [token]
let shape = "rect"
let after_shape = rest
if str_len(rest) > 0 {
let lead: String = str_char_at(rest, 0)
if lead == "[" {
let close: Int = str_index_of(rest, "]")
if close < 0 {
set_error("unclosed `[` in shape token", line_no, line)
return 0
}
let token: String = str_slice(rest, 1, close)
let parsed_shape: String = shape_from_token(token)
if str_eq(parsed_shape, "") {
set_error("unknown shape `" + token + "`", line_no, line)
return 0
}
let shape = parsed_shape
let after_shape = str_trim(str_slice(rest, close + 1, str_len(rest)))
}
}
// Optional quoted label.
let quoted: Map<String, Any> = parse_quoted(after_shape)
let label = raw_id
let ok: Bool = quoted["ok"]
if ok {
let label = quoted["value"]
}
store_node(id, label, shape)
register_node_in_group(id)
1
}
fn parse_edge_stmt(line_no: Int, line: String) -> Int {
let parts: Map<String, Any> = extract_edge_parts(line)
let ok: Bool = parts["ok"]
if !ok {
set_error("malformed edge — expected `->` `-->` `<->` or `-/->`", line_no, line)
return 0
}
let from_str: String = parts["from_str"]
let rest_str: String = parts["rest"]
let kind: String = parts["kind"]
let src: String = sanitize_id(str_trim(from_str))
let rest_t: String = str_trim(rest_str)
let id_split: Map<String, Any> = split_identifier(rest_t)
let to_raw: String = id_split["id"]
if str_eq(to_raw, "") {
set_error("edge missing target node id", line_no, line)
return 0
}
let dst: String = sanitize_id(to_raw)
let label_rest: String = str_trim(id_split["rest"])
let quoted: Map<String, Any> = parse_quoted(label_rest)
let label = ""
let qok: Bool = quoted["ok"]
if qok {
let label = quoted["value"]
}
store_edge(src, dst, label, kind)
1
}
fn parse_group_open(line_no: Int, line: String, rest: String) -> Int {
// Strip trailing `{`.
let trimmed: String = str_trim(rest)
let n: Int = str_len(trimmed)
let body = trimmed
if n > 0 {
let last: String = str_char_at(trimmed, n - 1)
if last == "{" {
let body = str_trim(str_slice(trimmed, 0, n - 1))
}
}
let id_split: Map<String, Any> = split_identifier(body)
let raw_id: String = id_split["id"]
if str_eq(raw_id, "") {
set_error("group declaration missing id", line_no, line)
return 0
}
let label_rest: String = str_trim(id_split["rest"])
let quoted: Map<String, Any> = parse_quoted(label_rest)
let label = raw_id
let qok: Bool = quoted["ok"]
if qok {
let label = quoted["value"]
}
open_group(raw_id, label, line_no)
1
}
fn parse_close_brace(line_no: Int) -> Int {
let frame: Map<String, Any> = close_group_frame()
let frame_ok: Bool = frame["ok"]
if !frame_ok {
set_error("unexpected `}` — no open group", line_no, "}")
return 0
}
store_group(frame["id"], frame["label"], frame["ids"])
1
}
fn parse_line_dispatch(line_no: Int, line: String) -> Int {
if line == "}" { return parse_close_brace(line_no) }
if str_starts_with(line, "title:") {
let after: String = str_trim(str_slice(line, 6, str_len(line)))
let q: Map<String, Any> = parse_quoted(after)
let qok: Bool = q["ok"]
if !qok {
set_error("expected quoted string after `title:`", line_no, line)
return 0
}
state_set("graph_title", q["value"])
return 1
}
if str_starts_with(line, "direction:") {
let after: String = str_trim(str_slice(line, 10, str_len(line)))
let dir: String = parse_direction(after)
if str_eq(dir, "") {
set_error("unknown direction — expected top-down, left-right, right-left, or bottom-up",
line_no, line)
return 0
}
state_set("graph_direction", dir)
return 1
}
if str_starts_with(line, "group ") {
let after: String = str_slice(line, 6, str_len(line))
return parse_group_open(line_no, line, after)
}
if is_edge_line(line) {
return parse_edge_stmt(line_no, line)
}
parse_node_stmt(line_no, line)
}
// Materialise accumulators into the final graph map
fn build_graph_value() -> Map<String, Any> {
let n_nodes: Int = st_get_int("node_count")
let nodes: [Map<String, Any>] = el_list_empty()
let i = 0
while i < n_nodes {
let s: String = int_to_str(i)
let node: Map<String, Any> = {
"id": state_get("node_id_" + s),
"label": state_get("node_label_" + s),
"shape": state_get("node_shape_" + s)
}
let nodes = native_list_append(nodes, node)
let i = i + 1
}
let n_edges: Int = st_get_int("edge_count")
let edges: [Map<String, Any>] = el_list_empty()
let i = 0
while i < n_edges {
let s: String = int_to_str(i)
let edge: Map<String, Any> = {
"from": state_get("edge_from_" + s),
"to": state_get("edge_to_" + s),
"label": state_get("edge_label_" + s),
"kind": state_get("edge_kind_" + s)
}
let edges = native_list_append(edges, edge)
let i = i + 1
}
let n_groups: Int = st_get_int("group_count")
let groups: [Map<String, Any>] = el_list_empty()
let i = 0
while i < n_groups {
let s: String = int_to_str(i)
let raw_ids: String = state_get("group_ids_" + s)
let id_list: [String] = list_decode(raw_ids)
let group: Map<String, Any> = {
"id": state_get("group_id_" + s),
"label": state_get("group_label_" + s),
"node_ids": id_list,
"direction": ""
}
let groups = native_list_append(groups, group)
let i = i + 1
}
{
"title": state_get("graph_title"),
"direction": state_get("graph_direction"),
"nodes": nodes,
"edges": edges,
"groups": groups
}
}
// Public entry point
fn arbor_parse(source: String) -> Map<String, Any> {
reset_state()
let lines: [Map<String, Any>] = preprocess(source)
let n: Int = el_list_len(lines)
let i = 0
let abort = false
while i < n {
if abort {
// skip error already recorded
} else {
let row: Map<String, Any> = get(lines, i)
let line_no: Int = row["no"]
let text: String = row["text"]
parse_line_dispatch(line_no, text)
if has_error() {
let abort = true
}
}
let i = i + 1
}
if !has_error() {
let depth: Int = st_get_int("group_stack_depth")
if depth > 0 {
let idx: Int = depth - 1
let id: String = state_get(group_frame_key(idx, "id"))
let line_no: Int = st_get_int(group_frame_key(idx, "line"))
set_error("unclosed group '" + id + "' — missing closing `}`",
line_no, "group " + id)
}
}
if has_error() {
return {
"error": state_get("parse_error"),
"line": st_get_int("parse_error_line"),
"text": state_get("parse_error_text")
}
}
build_graph_value()
}
// Smoke test
fn fail_msg(label: String, got: String, want: String) -> Int {
println("FAIL " + label + " got=[" + got + "] want=[" + want + "]")
state_set("smoke_failures", "1")
0
}
fn check_eq(label: String, got: String, want: String) -> Int {
if got == want {
println("ok " + label)
return 1
}
fail_msg(label, got, want)
}
// Helper: a graph map is in the error state iff it has a non-empty "error".
fn parse_failed(g: Map<String, Any>) -> Bool {
let m: String = g["error"]
if str_eq(m, "") { return false }
// map_get returns NULL for missing keys; str_eq treats two NULLs as equal
// and NULL vs "" as not equal guard explicitly.
if str_len(m) == 0 { return false }
true
}
let src1 = "title: \"Test\"\ndirection: left-right\n\napi [rounded] \"REST API\"\ndb [cylinder] \"Postgres\"\n\napi -> db \"reads\""
let g1: Map<String, Any> = arbor_parse(src1)
if parse_failed(g1) {
println("FAIL parse 1: " + g1["error"])
state_set("smoke_failures", "1")
}
check_eq("title parsed", g1["title"], "Test")
check_eq("direction parsed", g1["direction"], "left-right")
let nodes1: [Map<String, Any>] = g1["nodes"]
let nn1: Int = el_list_len(nodes1)
check_eq("two nodes", int_to_str(nn1), "2")
let edges1: [Map<String, Any>] = g1["edges"]
let ne1: Int = el_list_len(edges1)
check_eq("one edge", int_to_str(ne1), "1")
let e0: Map<String, Any> = get(edges1, 0)
check_eq("edge from", e0["from"], "api")
check_eq("edge to", e0["to"], "db")
check_eq("edge label", e0["label"], "reads")
check_eq("edge kind", e0["kind"], "solid")
let n0: Map<String, Any> = get(nodes1, 0)
check_eq("node 0 shape", n0["shape"], "rounded")
check_eq("node 0 label", n0["label"], "REST API")
// Test edge varieties
let src2 = "a \"A\"\nb \"B\"\na -> b\na --> b\na -/-> b\na <-> b"
let g2: Map<String, Any> = arbor_parse(src2)
let edges2: [Map<String, Any>] = g2["edges"]
check_eq("4 edges parsed", int_to_str(el_list_len(edges2)), "4")
let kinds = ""
let i = 0
while i < el_list_len(edges2) {
let e: Map<String, Any> = get(edges2, i)
let k: String = e["kind"]
let kinds = kinds + k + ","
let i = i + 1
}
check_eq("edge kinds", kinds, "solid,dashed,forbidden,bidirectional,")
// Groups
let src3 = "group core \"Application Core\" {\n api [rounded] \"REST API\"\n svc \"Business Logic\"\n}\nstandalone \"Out\""
let g3: Map<String, Any> = arbor_parse(src3)
let groups3: [Map<String, Any>] = g3["groups"]
check_eq("one group", int_to_str(el_list_len(groups3)), "1")
let grp0: Map<String, Any> = get(groups3, 0)
check_eq("group label", grp0["label"], "Application Core")
let gnids: [String] = grp0["node_ids"]
check_eq("group has 2 members", int_to_str(el_list_len(gnids)), "2")
let nodes3: [Map<String, Any>] = g3["nodes"]
check_eq("3 total nodes (incl standalone)",
int_to_str(el_list_len(nodes3)), "3")
// Error: unknown shape
let src4 = "node [hexagon] \"X\""
let g4: Map<String, Any> = arbor_parse(src4)
let err4: String = g4["error"]
if str_eq(err4, "") {
println("FAIL expected error for unknown shape")
state_set("smoke_failures", "1")
} else {
if str_contains(err4, "hexagon") {
println("ok error mentions hexagon: " + err4)
} else {
println("FAIL error wording: " + err4)
state_set("smoke_failures", "1")
}
}
// Error: unclosed group
let src5 = "group g \"G\" {\n a \"A\"\n"
let g5: Map<String, Any> = arbor_parse(src5)
let err5: String = g5["error"]
if str_eq(err5, "") {
println("FAIL expected unclosed-group error")
state_set("smoke_failures", "1")
} else {
if str_contains(err5, "unclosed") {
println("ok unclosed group detected")
} else {
println("FAIL unclosed error wording: " + err5)
state_set("smoke_failures", "1")
}
}
// Comments and inline comments
let src6 = "// header\na \"A\" // trailing\nb \"B\""
let g6: Map<String, Any> = arbor_parse(src6)
check_eq("comments stripped", int_to_str(el_list_len(g6["nodes"])), "2")
// Empty input
let g7: Map<String, Any> = arbor_parse("")
check_eq("empty graph nodes", int_to_str(el_list_len(g7["nodes"])), "0")
check_eq("empty graph default direction", g7["direction"], "top-down")
println("")
let f: String = state_get("smoke_failures")
if str_eq(f, "1") {
println("arbor-parse: FAILED")
exit_program(1)
} else {
println("arbor-parse: ok")
}
-21
View File
@@ -1,21 +0,0 @@
// arbor-render SVG renderer. Consumes a diagram graph + layout result and
// emits an SVG document. PNG rasterization is not provided in this vessel
// because the El runtime does not expose a vector-to-raster primitive yet
// (see report).
vessel "arbor-render" {
version "0.1.0"
description "SVG renderer for Arbor diagrams"
authors ["Neuron Technologies"]
edition "2026"
}
dependencies {
arbor-core "0.1"
arbor-layout "0.1"
}
build {
entry "src/main.el"
output "dist/"
}
-575
View File
@@ -1,575 +0,0 @@
// arbor-render SVG emission from a laid-out diagram.
//
// Entry point:
// fn arbor_render_svg(graph: Map, layout: Map, forbidden: [String]) -> String
//
// The graph is the lowered (diagram-form) shape produced by arbor-core /
// arbor-diagram (`title`, `direction`, `nodes`, `edges`, `groups`). The
// layout is whatever arbor-layout returned: `node_pos_<id>`, `node_size_<id>`,
// `group_bounds_<id>`, `node_ids`, `group_ids`, `canvas`.
//
// `forbidden` is a list of "from->to" key strings same format as
// arbor-core's collect_forbidden(). The Rust crate threaded a HashSet
// through; El threads a list and we linear-scan.
//
// SVG is text emission straightforward El. Every float coordinate is
// passed through format_float(_, 1) for stable output.
//
// PNG render is intentionally out of scope
// The Rust crate rasterises via resvg tiny_skia png. The El runtime
// today exposes no equivalent: there is no resvg, no usvg, no font rasterer,
// no PNG encoder, no path-fill code. fs_write writes text only there is
// no binary write primitive. arbor_render_png() returns an error map in El
// until the runtime grows a rasterer (see "runtime gaps" in the report).
// Colour palette (matches the Rust constants exactly)
fn col_node_fill() -> String { "#ffffff" }
fn col_node_stroke() -> String { "#334155" }
fn col_primary_fill() -> String { "#0052A0" }
fn col_primary_text() -> String { "#ffffff" }
fn col_node_text() -> String { "#0D0D14" }
fn col_edge() -> String { "#64748B" }
fn col_edge_forbidden() -> String { "#DC2626" }
fn col_group_fill() -> String { "rgba(0,0,0,0.03)" }
fn col_group_stroke() -> String { "#CBD5E1" }
fn col_group_text() -> String { "#64748B" }
fn col_edge_label() -> String { "#64748B" }
// XML escape
fn esc(s: String) -> String {
let r1: String = str_replace(s, "&", "&amp;")
let r2: String = str_replace(r1, "<", "&lt;")
let r3: String = str_replace(r2, ">", "&gt;")
let r4: String = str_replace(r3, "\"", "&quot;")
r4
}
// Float to "%.1f" the Rust pt() helper.
fn pt(v: el_val_t) -> String {
format_float(v, 1)
}
// Float arithmetic helpers float_to_int / int_to_float trip through Int,
// which is exact for the integer-valued floats used by the layout pass.
fn fadd(a: el_val_t, b: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
let bi: Int = float_to_int(b)
int_to_float(ai + bi)
}
fn fsub(a: el_val_t, b: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
let bi: Int = float_to_int(b)
int_to_float(ai - bi)
}
fn fdiv2(a: el_val_t) -> el_val_t {
let ai: Int = float_to_int(a)
int_to_float(ai / 2)
}
fn fmid(a: el_val_t, b: el_val_t) -> el_val_t {
fdiv2(fadd(a, b))
}
// forbidden-edge linear lookup
fn forbidden_key(from: String, to: String) -> String {
from + "->" + to
}
fn forbidden_contains(set: [String], src: String, dst: String) -> Bool {
let key: String = forbidden_key(src, dst)
let n: Int = el_list_len(set)
let i = 0
while i < n {
let s: String = get(set, i)
if str_eq(s, key) { return true }
let i = i + 1
}
false
}
// Arrow marker defs
fn arrow_defs() -> String {
let s = "\n <marker id=\"ah\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
let s = s + " </marker>\n"
let s = s + " <marker id=\"ah-bi\" markerWidth=\"10\" markerHeight=\"7\" refX=\"1\" refY=\"3.5\" orient=\"auto-start-reverse\">\n"
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
let s = s + " </marker>\n"
let s = s + " <marker id=\"ah-red\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge_forbidden() + "\"/>\n"
let s = s + " </marker>"
s
}
// Node rendering
fn render_node(buf: String, node: Map<String, Any>, layout: Map<String, Any>) -> String {
let nid: String = node["id"]
let pos: Map<String, Any> = el_map_get(layout, "node_pos_" + nid)
let sz: Map<String, Any> = el_map_get(layout, "node_size_" + nid)
let cx: el_val_t = pos["x"]
let cy: el_val_t = pos["y"]
let w: el_val_t = sz["w"]
let h: el_val_t = sz["h"]
let x: el_val_t = fsub(cx, fdiv2(w))
let y: el_val_t = fsub(cy, fdiv2(h))
let fill_in: String = node["style_fill"]
let stroke_in: String = node["style_stroke"]
let color_in: String = node["style_color"]
let fill = col_node_fill()
if str_len(fill_in) > 0 { let fill = fill_in }
let stroke = col_node_stroke()
if str_len(stroke_in) > 0 { let stroke = stroke_in }
let text_col = col_node_text()
if str_len(color_in) > 0 { let text_col = color_in }
let shape: String = node["shape"]
let buf = buf
if str_eq(shape, "rectangle") {
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
let buf = buf + "\" rx=\"4\" fill=\"" + fill + "\" stroke=\"" + stroke
let buf = buf + "\" stroke-width=\"1.5\"/>\n"
}
if str_eq(shape, "rounded_rect") {
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
let buf = buf + "\" rx=\"20\" fill=\"" + fill + "\" stroke=\"" + stroke
let buf = buf + "\" stroke-width=\"1.5\"/>\n"
}
if str_eq(shape, "stadium") {
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
let buf = buf + "\" rx=\"" + pt(fdiv2(h)) + "\" fill=\"" + fill
let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
}
if str_eq(shape, "cylinder") {
// body: rect from y+ry to bottom; ry ≈ h/6 (Rust uses h*0.18, we use h/6
// to stay in integer arithmetic visually indistinguishable on the
// canvas sizes the layout produces).
let hi: Int = float_to_int(h)
let ry: el_val_t = int_to_float(hi / 6)
let body_y: el_val_t = fadd(y, ry)
let body_h: el_val_t = fsub(h, ry)
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(body_y)
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(body_h)
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
// top ellipse
let buf = buf + " <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(body_y)
let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
// bottom ellipse
let bot_y: el_val_t = fadd(y, h)
let buf = buf + " <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(bot_y)
let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
}
if str_eq(shape, "diamond") {
let hw: el_val_t = fdiv2(w)
let hh: el_val_t = fdiv2(h)
let buf = buf + " <polygon points=\""
let buf = buf + pt(cx) + "," + pt(fsub(cy, hh)) + " "
let buf = buf + pt(fadd(cx, hw)) + "," + pt(cy) + " "
let buf = buf + pt(cx) + "," + pt(fadd(cy, hh)) + " "
let buf = buf + pt(fsub(cx, hw)) + "," + pt(cy)
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
}
// Label.
let label: String = node["label"]
let buf = buf + " <text x=\"" + pt(cx) + "\" y=\"" + pt(cy)
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\">"
let buf = buf + esc(label) + "</text>\n"
// Sublabel Rust's DiagramNode stores Option<String>; El uses "" sentinel.
let sub: String = node["sublabel"]
if str_len(sub) > 0 {
let sub_y: el_val_t = fadd(cy, int_to_float(14))
let buf = buf + " <text x=\"" + pt(cx) + "\" y=\"" + pt(sub_y)
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\" font-size=\"10\">"
let buf = buf + esc(sub) + "</text>\n"
}
buf
}
// Edge rendering
//
// We emit a straight line from one node centre to the other and let the
// browser draw it; the Rust crate renders cubic bezier paths but the runtime
// has no robust math layer, and the rectangles are large enough that
// straight edges read clearly. (See "runtime gaps".)
fn render_edge(buf: String, edge: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
let from_id: String = edge["from"]
let to_id: String = edge["to"]
let from_pos: Map<String, Any> = el_map_get(layout, "node_pos_" + from_id)
let to_pos: Map<String, Any> = el_map_get(layout, "node_pos_" + to_id)
let fx: el_val_t = from_pos["x"]
let fy: el_val_t = from_pos["y"]
let tx: el_val_t = to_pos["x"]
let ty: el_val_t = to_pos["y"]
let is_forbidden: Bool = forbidden_contains(forbidden, from_id, to_id)
let stroke = col_edge()
if is_forbidden { let stroke = col_edge_forbidden() }
let line: String = edge["line"]
let arrow: String = edge["arrow"]
let dash_attr = ""
if str_eq(line, "dashed") { let dash_attr = " stroke-dasharray=\"5,3\"" }
if str_eq(line, "dotted") { let dash_attr = " stroke-dasharray=\"2,2\"" }
let marker_start = ""
if str_eq(arrow, "both") { let marker_start = " marker-start=\"url(#ah-bi)\"" }
if str_eq(arrow, "backward") { let marker_start = " marker-start=\"url(#ah-bi)\"" }
let marker_end = " marker-end=\"url(#ah)\""
if is_forbidden { let marker_end = " marker-end=\"url(#ah-red)\"" }
if str_eq(arrow, "none") { let marker_end = "" }
if str_eq(arrow, "backward") { let marker_end = "" }
let buf = buf + " <line x1=\"" + pt(fx) + "\" y1=\"" + pt(fy)
let buf = buf + "\" x2=\"" + pt(tx) + "\" y2=\"" + pt(ty)
let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\""
let buf = buf + dash_attr + marker_start + marker_end + "/>\n"
// Forbidden marker circle-X at midpoint.
if is_forbidden {
let mx: el_val_t = fmid(fx, tx)
let my: el_val_t = fmid(fy, ty)
let r: el_val_t = int_to_float(7)
let buf = buf + " <circle cx=\"" + pt(mx) + "\" cy=\"" + pt(my)
let buf = buf + "\" r=\"" + pt(r) + "\" fill=\"white\" stroke=\""
let buf = buf + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
let off: el_val_t = int_to_float(4)
let buf = buf + " <line x1=\"" + pt(fsub(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
let buf = buf + "\" x2=\"" + pt(fadd(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
let buf = buf + " <line x1=\"" + pt(fadd(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
let buf = buf + "\" x2=\"" + pt(fsub(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
}
// Edge label
let label: String = edge["label"]
if str_len(label) > 0 {
let mx: el_val_t = fmid(fx, tx)
let my: el_val_t = fmid(fy, ty)
let lw: el_val_t = int_to_float(str_len(label) * 7 + 8)
let lh: el_val_t = int_to_float(16)
let buf = buf + " <rect x=\"" + pt(fsub(mx, fdiv2(lw))) + "\" y=\"" + pt(fsub(my, fdiv2(lh)))
let buf = buf + "\" width=\"" + pt(lw) + "\" height=\"" + pt(lh)
let buf = buf + "\" rx=\"3\" fill=\"white\" opacity=\"0.85\"/>\n"
let buf = buf + " <text x=\"" + pt(mx) + "\" y=\"" + pt(my)
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
let buf = buf + " class=\"arbor-edge-label\">" + esc(label) + "</text>\n"
}
buf
}
// Group rendering
fn render_group(buf: String, group: Map<String, Any>, layout: Map<String, Any>) -> String {
let gid: String = group["id"]
let bounds: Map<String, Any> = el_map_get(layout, "group_bounds_" + gid)
// Layout may not have bounds for empty groups defensive.
let bx_check: el_val_t = bounds["x"]
if float_to_int(bx_check) == 0 {
// Could be a real 0; cheaper to skip via presence check on group_ids.
}
let bx: el_val_t = bounds["x"]
let by: el_val_t = bounds["y"]
let bw: el_val_t = bounds["w"]
let bh: el_val_t = bounds["h"]
let buf = buf + " <rect x=\"" + pt(bx) + "\" y=\"" + pt(by)
let buf = buf + "\" width=\"" + pt(bw) + "\" height=\"" + pt(bh)
let buf = buf + "\" rx=\"8\" fill=\"" + col_group_fill() + "\" stroke=\""
let buf = buf + col_group_stroke() + "\" stroke-width=\"1\" stroke-dasharray=\"4,3\"/>\n"
// Group label in the top-left corner.
let lx: el_val_t = fadd(bx, int_to_float(8))
let ly: el_val_t = fadd(by, int_to_float(14))
let label: String = group["label"]
let buf = buf + " <text x=\"" + pt(lx) + "\" y=\"" + pt(ly)
let buf = buf + "\" class=\"arbor-group-label\">" + esc(label) + "</text>\n"
buf
}
// Public entry point
fn arbor_render_svg(graph: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
let canvas: Map<String, Any> = el_map_get(layout, "canvas")
let cw: el_val_t = canvas["w"]
let ch: el_val_t = canvas["h"]
let buf = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + pt(cw)
let buf = buf + "\" height=\"" + pt(ch) + "\" viewBox=\"0 0 " + pt(cw) + " " + pt(ch) + "\">\n"
let buf = buf + " <defs>"
let buf = buf + arrow_defs()
let buf = buf + "\n <style>\n"
let buf = buf + " .arbor-node-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; }\n"
let buf = buf + " .arbor-group-label { font-family: 'Helvetica Neue', Helvetica, Arial, monospace; font-size: 10px; fill: " + col_group_text() + "; letter-spacing: 0.08em; }\n"
let buf = buf + " .arbor-edge-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 11px; fill: " + col_edge_label() + "; }\n"
let buf = buf + " </style>\n"
let buf = buf + " </defs>\n"
// Groups first (behind everything).
let buf = buf + " <!-- Groups -->\n"
let groups: [Map<String, Any>] = graph["groups"]
let gn: Int = el_list_len(groups)
let i = 0
while i < gn {
let g: Map<String, Any> = get(groups, i)
let gid: String = g["id"]
// Only render groups the layout actually placed.
let gids: [String] = el_map_get(layout, "group_ids")
let placed = false
let j = 0
while j < el_list_len(gids) {
if str_eq(get(gids, j), gid) { let placed = true }
let j = j + 1
}
if placed {
let buf = render_group(buf, g, layout)
}
let i = i + 1
}
// Edges
let buf = buf + " <!-- Edges -->\n"
let edges: [Map<String, Any>] = graph["edges"]
let en: Int = el_list_len(edges)
let i = 0
while i < en {
let e: Map<String, Any> = get(edges, i)
let buf = render_edge(buf, e, layout, forbidden)
let i = i + 1
}
// Nodes
let buf = buf + " <!-- Nodes -->\n"
let nodes: [Map<String, Any>] = graph["nodes"]
let nn: Int = el_list_len(nodes)
let i = 0
while i < nn {
let n: Map<String, Any> = get(nodes, i)
let buf = render_node(buf, n, layout)
let i = i + 1
}
// Title
let title: String = graph["title"]
if str_len(title) > 0 {
let title_x: el_val_t = fdiv2(cw)
let buf = buf + " <text x=\"" + pt(title_x) + "\" y=\"22\" text-anchor=\"middle\""
let buf = buf + " font-family=\"'Helvetica Neue', Helvetica, Arial, sans-serif\""
let buf = buf + " font-size=\"15\" font-weight=\"600\" fill=\"" + col_node_text() + "\">"
let buf = buf + esc(title) + "</text>\n"
}
let buf = buf + "</svg>\n"
buf
}
// PNG not implemented; the runtime has no SVG rasterizer or PNG encoder.
// Returns an error map that callers can inspect via map["error"].
fn arbor_render_png(graph: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> Map<String, Any> {
{
"error": "PNG rasterization not available in El runtime — install a runtime image library or use the Rust binary"
}
}
// Smoke test
fn fail(label: String, msg: String) -> Int {
println("FAIL " + label + ": " + msg)
state_set("smoke_failures", "1")
0
}
fn check_contains(label: String, haystack: String, needle: String) -> Int {
if str_contains(haystack, needle) {
println("ok " + label)
return 1
}
fail(label, "missing [" + needle + "]")
}
fn check_not_contains(label: String, haystack: String, needle: String) -> Int {
if str_contains(haystack, needle) {
return fail(label, "should not contain [" + needle + "]")
}
println("ok " + label)
1
}
fn make_test_node(id: String, label: String, shape: String) -> Map<String, Any> {
{
"id": id, "label": label, "sublabel": "",
"shape": shape,
"style_fill": "", "style_stroke": "", "style_color": ""
}
}
fn make_test_edge(src: String, dst: String, line: String, arrow: String, label: String) -> Map<String, Any> {
{
"from": src, "to": dst, "label": label,
"line": line, "arrow": arrow
}
}
fn make_test_pos(x: Int, y: Int) -> Map<String, Any> {
{ "x": int_to_float(x), "y": int_to_float(y) }
}
fn make_test_size(w: Int, h: Int) -> Map<String, Any> {
{ "w": int_to_float(w), "h": int_to_float(h) }
}
// Build a minimal layout map by hand.
fn build_layout(node_ids: [String], group_ids: [String], cw: Int, ch: Int) -> Map<String, Any> {
let r: Map<String, Any> = el_map_new(0)
let r = el_map_set(r, "node_ids", node_ids)
let r = el_map_set(r, "group_ids", group_ids)
let r = el_map_set(r, "canvas", { "w": int_to_float(cw), "h": int_to_float(ch) })
r
}
let n_a: Map<String, Any> = make_test_node("a", "Node A", "rectangle")
let n_b: Map<String, Any> = make_test_node("b", "Node B", "rectangle")
let e_ab: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "")
let nodes: [Map<String, Any>] = native_list_empty()
let nodes = native_list_append(nodes, n_a)
let nodes = native_list_append(nodes, n_b)
let edges: [Map<String, Any>] = native_list_empty()
let edges = native_list_append(edges, e_ab)
let groups: [Map<String, Any>] = native_list_empty()
let g: Map<String, Any> = {
"title": "Test", "direction": "top-down",
"nodes": nodes, "edges": edges, "groups": groups
}
let nid_list: [String] = native_list_empty()
let nid_list = native_list_append(nid_list, "a")
let nid_list = native_list_append(nid_list, "b")
let gid_list: [String] = native_list_empty()
let layout: Map<String, Any> = build_layout(nid_list, gid_list, 400, 300)
let layout = el_map_set(layout, "node_pos_a", make_test_pos(100, 60))
let layout = el_map_set(layout, "node_pos_b", make_test_pos(100, 200))
let layout = el_map_set(layout, "node_size_a", make_test_size(120, 40))
let layout = el_map_set(layout, "node_size_b", make_test_size(120, 40))
let forbidden: [String] = native_list_empty()
let svg: String = arbor_render_svg(g, layout, forbidden)
check_contains("svg starts with <svg", svg, "<svg xmlns=")
check_contains("svg ends with </svg>", svg, "</svg>")
check_contains("svg contains node label", svg, "Node A")
check_contains("svg contains title", svg, ">Test</text>")
check_contains("svg has rect for rectangle node", svg, "<rect")
check_contains("svg has line for edge", svg, "<line")
check_contains("svg has arrow marker def", svg, "id=\"ah\"")
// Escape test
let n_esc: Map<String, Any> = make_test_node("x", "A & B <C>", "rectangle")
let nodes2: [Map<String, Any>] = native_list_empty()
let nodes2 = native_list_append(nodes2, n_esc)
let g2: Map<String, Any> = {
"title": "Test <Title>", "direction": "top-down",
"nodes": nodes2, "edges": native_list_empty(), "groups": native_list_empty()
}
let nid2: [String] = native_list_empty()
let nid2 = native_list_append(nid2, "x")
let layout2: Map<String, Any> = build_layout(nid2, native_list_empty(), 200, 100)
let layout2 = el_map_set(layout2, "node_pos_x", make_test_pos(80, 40))
let layout2 = el_map_set(layout2, "node_size_x", make_test_size(120, 40))
let svg2: String = arbor_render_svg(g2, layout2, native_list_empty())
check_contains("escapes ampersand", svg2, "&amp;")
check_contains("escapes <", svg2, "&lt;")
check_not_contains("no raw <C>", svg2, "<C>")
// Forbidden edge
let e_fb: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "")
let edges3: [Map<String, Any>] = native_list_empty()
let edges3 = native_list_append(edges3, e_fb)
let g3: Map<String, Any> = {
"title": "F", "direction": "top-down",
"nodes": nodes, "edges": edges3, "groups": native_list_empty()
}
let fb: [String] = native_list_empty()
let fb = native_list_append(fb, forbidden_key("a", "b"))
let svg3: String = arbor_render_svg(g3, layout, fb)
check_contains("forbidden uses red marker", svg3, "ah-red")
check_contains("forbidden colour present", svg3, col_edge_forbidden())
// Diamond shape polygon
let n_d: Map<String, Any> = make_test_node("d", "Decide", "diamond")
let g4: Map<String, Any> = {
"title": "", "direction": "top-down",
"nodes": native_list_append(native_list_empty(), n_d),
"edges": native_list_empty(), "groups": native_list_empty()
}
let nid4: [String] = native_list_append(native_list_empty(), "d")
let layout4: Map<String, Any> = build_layout(nid4, native_list_empty(), 200, 100)
let layout4 = el_map_set(layout4, "node_pos_d", make_test_pos(80, 50))
let layout4 = el_map_set(layout4, "node_size_d", make_test_size(120, 40))
let svg4: String = arbor_render_svg(g4, layout4, native_list_empty())
check_contains("diamond uses polygon", svg4, "<polygon")
// Cylinder shape ellipses
let n_cy: Map<String, Any> = make_test_node("cy", "DB", "cylinder")
let g5: Map<String, Any> = {
"title": "", "direction": "top-down",
"nodes": native_list_append(native_list_empty(), n_cy),
"edges": native_list_empty(), "groups": native_list_empty()
}
let nid5: [String] = native_list_append(native_list_empty(), "cy")
let layout5: Map<String, Any> = build_layout(nid5, native_list_empty(), 200, 100)
let layout5 = el_map_set(layout5, "node_pos_cy", make_test_pos(80, 50))
let layout5 = el_map_set(layout5, "node_size_cy", make_test_size(120, 40))
let svg5: String = arbor_render_svg(g5, layout5, native_list_empty())
check_contains("cylinder uses ellipse", svg5, "<ellipse")
// Dashed edge
let e_dash: Map<String, Any> = make_test_edge("a", "b", "dashed", "forward", "")
let g6: Map<String, Any> = {
"title": "", "direction": "top-down",
"nodes": nodes, "edges": native_list_append(native_list_empty(), e_dash),
"groups": native_list_empty()
}
let svg6: String = arbor_render_svg(g6, layout, native_list_empty())
check_contains("dashed line dasharray", svg6, "stroke-dasharray=\"5,3\"")
// PNG returns an error map
let png: Map<String, Any> = arbor_render_png(g, layout, native_list_empty())
let err: String = png["error"]
if str_len(err) > 0 {
println("ok PNG returns error map")
} else {
println("FAIL PNG should have returned error")
state_set("smoke_failures", "1")
}
println("")
let f: String = state_get("smoke_failures")
if str_eq(f, "1") {
println("arbor-render: FAILED")
exit_program(1)
} else {
println("arbor-render: ok")
}
+765 -2124
View File
File diff suppressed because it is too large Load Diff
Vendored Executable
BIN
View File
Binary file not shown.
Vendored Executable
BIN
View File
Binary file not shown.
+437 -35
View File
@@ -129,6 +129,9 @@ el_val_t js_str_lit(el_val_t s);
el_val_t js_emit_line(el_val_t line);
el_val_t js_emit_blank(void);
el_val_t js_binop(el_val_t op);
el_val_t js_is_async_builtin(el_val_t name);
el_val_t js_register_async_fn(el_val_t name);
el_val_t js_is_async_fn(el_val_t name);
el_val_t js_is_int_name(el_val_t name);
el_val_t js_add_int_name(el_val_t name);
el_val_t js_build_int_names_for_params(el_val_t params);
@@ -149,17 +152,32 @@ el_val_t js_cg_fn(el_val_t stmt);
el_val_t js_is_fndef(el_val_t stmt);
el_val_t js_is_top_level_decl(el_val_t stmt);
el_val_t codegen_js(el_val_t stmts, el_val_t source);
el_val_t codegen_js_bundle(el_val_t stmts, el_val_t source, el_val_t runtime_content);
el_val_t codegen_js_inner(el_val_t stmts, el_val_t source, el_val_t bundle_mode, el_val_t runtime_content);
el_val_t js_strip_es_exports(el_val_t content);
el_val_t compile(el_val_t source);
el_val_t compile_js(el_val_t source);
el_val_t compile_js_with_bundle(el_val_t source, el_val_t runtime_path);
el_val_t compile_dispatch(el_val_t tgt, el_val_t source);
el_val_t compile_dispatch_bundle(el_val_t tgt, el_val_t source, el_val_t runtime_path);
el_val_t detect_target(el_val_t argv);
el_val_t strip_flags(el_val_t argv);
el_val_t detect_emit_header(el_val_t argv);
el_val_t detect_bundle(el_val_t argv);
el_val_t detect_minify(el_val_t argv);
el_val_t detect_obfuscate(el_val_t argv);
el_val_t make_temp_path(el_val_t suffix);
el_val_t js_reserved_names(void);
el_val_t find_node_tool(el_val_t tool_name, el_val_t src_dir);
el_val_t apply_minify(el_val_t js_path, el_val_t out_path, el_val_t src_dir);
el_val_t apply_obfuscate(el_val_t js_path, el_val_t out_path, el_val_t src_dir);
el_val_t resolve_runtime_path(el_val_t src_path);
el_val_t type_node_to_el(el_val_t t);
el_val_t emit_header(el_val_t stmts, el_val_t hdr_path);
el_val_t dirname_of(el_val_t path);
el_val_t parse_import_line(el_val_t trimmed, el_val_t dir);
el_val_t resolve_imports(el_val_t src_path);
el_val_t run_with_postprocess(el_val_t tgt, el_val_t source, el_val_t src_path, el_val_t do_bundle, el_val_t do_obfuscate, el_val_t argc, el_val_t positional);
el_val_t lex_is_digit(el_val_t ch) {
if (str_eq(ch, EL_STR("0"))) {
@@ -1474,6 +1492,11 @@ el_val_t parse_pattern(el_val_t tokens, el_val_t pos) {
if (str_eq(v, EL_STR("_"))) {
return make_result(el_map_new(1, "pattern", EL_STR("Wildcard")), (pos + 1));
}
el_val_t next_k = tok_kind(tokens, (pos + 1));
if (str_eq(next_k, EL_STR("ColonColon"))) {
el_val_t variant_name = tok_value(tokens, (pos + 2));
return make_result(el_map_new(3, "pattern", EL_STR("Variant"), "enum_name", v, "variant", variant_name), (pos + 3));
}
return make_result(el_map_new(2, "pattern", EL_STR("Binding"), "name", v), (pos + 1));
}
if (str_eq(k, EL_STR("Int"))) {
@@ -1855,6 +1878,10 @@ el_val_t parse_stmt(el_val_t tokens, el_val_t pos) {
el_val_t p = (pos + 1);
el_val_t name = tok_value(tokens, p);
p = (p + 1);
el_val_t pk = tok_kind(tokens, p);
if (str_eq(pk, EL_STR("Eq"))) {
p = (p + 1);
}
p = expect(tokens, p, EL_STR("LBrace"));
el_val_t fields = native_list_empty();
el_val_t running = 1;
@@ -2923,7 +2950,12 @@ el_val_t cg_match(el_val_t expr) {
}
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if ("), subj_var), EL_STR(" == ")), bv), EL_STR(") { ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")));
} else {
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{ "), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")));
if (str_eq(pkind, EL_STR("Variant"))) {
el_val_t variant = el_get_field(pat, EL_STR("variant"));
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if (str_eq("), subj_var), EL_STR(", EL_STR(")), c_str_lit(variant)), EL_STR("))) { ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")));
} else {
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{ "), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } ")));
}
}
}
}
@@ -4569,6 +4601,15 @@ el_val_t builtin_arity(el_val_t name) {
if (str_eq(name, EL_STR("exit_program"))) {
return 1;
}
if (str_eq(name, EL_STR("getpid_now"))) {
return 0;
}
if (str_eq(name, EL_STR("stdout_to_file"))) {
return 1;
}
if (str_eq(name, EL_STR("stdout_restore"))) {
return 0;
}
if (str_eq(name, EL_STR("exec_command"))) {
return 1;
}
@@ -5594,6 +5635,49 @@ el_val_t js_binop(el_val_t op) {
return 0;
}
el_val_t js_is_async_builtin(el_val_t name) {
if (str_eq(name, EL_STR("http_get"))) {
return 1;
}
if (str_eq(name, EL_STR("http_post"))) {
return 1;
}
if (str_eq(name, EL_STR("http_post_json"))) {
return 1;
}
if (str_eq(name, EL_STR("http_get_with_headers"))) {
return 1;
}
if (str_eq(name, EL_STR("http_post_with_headers"))) {
return 1;
}
return 0;
return 0;
}
el_val_t js_register_async_fn(el_val_t name) {
el_val_t csv = state_get(EL_STR("__js_async_fns"));
if (str_eq(csv, EL_STR(""))) {
csv = EL_STR(",");
}
el_val_t key = el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","));
if (str_contains(csv, key)) {
return 1;
}
state_set(EL_STR("__js_async_fns"), el_str_concat(el_str_concat(csv, name), EL_STR(",")));
return 1;
return 0;
}
el_val_t js_is_async_fn(el_val_t name) {
el_val_t csv = state_get(EL_STR("__js_async_fns"));
if (str_eq(csv, EL_STR(""))) {
return 0;
}
return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(",")));
return 0;
}
el_val_t js_is_int_name(el_val_t name) {
el_val_t csv = state_get(EL_STR("__js_int_names"));
if (str_eq(csv, EL_STR(""))) {
@@ -5937,7 +6021,14 @@ el_val_t js_cg_expr(el_val_t expr) {
el_val_t args_c = str_join(args_parts, EL_STR(", "));
if (str_eq(func_kind, EL_STR("Ident"))) {
el_val_t fn_name = el_get_field(func, EL_STR("name"));
return el_str_concat(el_str_concat(el_str_concat(fn_name, EL_STR("(")), args_c), EL_STR(")"));
el_val_t call_expr = el_str_concat(el_str_concat(el_str_concat(fn_name, EL_STR("(")), args_c), EL_STR(")"));
if (js_is_async_builtin(fn_name)) {
return el_str_concat(EL_STR("await "), call_expr);
}
if (js_is_async_fn(fn_name)) {
return el_str_concat(EL_STR("await "), call_expr);
}
return call_expr;
}
if (str_eq(func_kind, EL_STR("Field"))) {
el_val_t obj = el_get_field(func, EL_STR("object"));
@@ -5954,6 +6045,12 @@ el_val_t js_cg_expr(el_val_t expr) {
if (str_eq(kind, EL_STR("Field"))) {
el_val_t obj = el_get_field(expr, EL_STR("object"));
el_val_t field = el_get_field(expr, EL_STR("field"));
el_val_t obj_kind = el_get_field(obj, EL_STR("expr"));
if (str_eq(obj_kind, EL_STR("Try"))) {
el_val_t inner = el_get_field(obj, EL_STR("inner"));
el_val_t inner_c = js_cg_expr(inner);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), inner_c), EL_STR(")?.[")), js_str_lit(field)), EL_STR("] ?? null"));
}
el_val_t obj_c = js_cg_expr(obj);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_get_field("), obj_c), EL_STR(", ")), js_str_lit(field)), EL_STR(")"));
}
@@ -5963,6 +6060,12 @@ el_val_t js_cg_expr(el_val_t expr) {
el_val_t obj_c = js_cg_expr(obj);
el_val_t idx_c = js_cg_expr(idx);
el_val_t idx_kind = el_get_field(idx, EL_STR("expr"));
el_val_t obj_kind = el_get_field(obj, EL_STR("expr"));
if (str_eq(obj_kind, EL_STR("Try"))) {
el_val_t inner = el_get_field(obj, EL_STR("inner"));
el_val_t inner_c = js_cg_expr(inner);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), inner_c), EL_STR(")?.[")), idx_c), EL_STR("] ?? null"));
}
if (str_eq(idx_kind, EL_STR("Str"))) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_get_field("), obj_c), EL_STR(", ")), idx_c), EL_STR(")"));
}
@@ -6069,7 +6172,12 @@ el_val_t js_cg_match(el_val_t expr) {
}
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if ("), subj_var), EL_STR(" === ")), bv), EL_STR(") return (")), body_c), EL_STR("); ")));
} else {
parts = native_list_append(parts, el_str_concat(el_str_concat(EL_STR("return ("), body_c), EL_STR("); ")));
if (str_eq(pkind, EL_STR("Variant"))) {
el_val_t variant = el_get_field(pat, EL_STR("variant"));
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if (str_eq("), subj_var), EL_STR(", ")), js_str_lit(variant)), EL_STR(")) return (")), body_c), EL_STR("); ")));
} else {
parts = native_list_append(parts, el_str_concat(el_str_concat(EL_STR("return ("), body_c), EL_STR("); ")));
}
}
}
}
@@ -6182,12 +6290,12 @@ el_val_t js_cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) {
}
if (str_eq(kind, EL_STR("CgiBlock"))) {
el_val_t cname = el_get_field(stmt, EL_STR("name"));
js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// cgi block '")), cname), EL_STR("' - no-op in JS target (server-side concept)")));
js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// cgi block '")), cname), EL_STR("' \xe2\x80\x94 no-op in JS target (server-side concept)")));
return declared;
}
if (str_eq(kind, EL_STR("ServiceBlock"))) {
el_val_t sname = el_get_field(stmt, EL_STR("name"));
js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// service block '")), sname), EL_STR("' - no-op in JS target")));
js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// service block '")), sname), EL_STR("' \xe2\x80\x94 no-op in JS target")));
return declared;
}
return declared;
@@ -6330,12 +6438,22 @@ el_val_t js_cg_fn(el_val_t stmt) {
el_val_t params = el_get_field(stmt, EL_STR("params"));
el_val_t body = el_get_field(stmt, EL_STR("body"));
el_val_t ret_type = el_get_field(stmt, EL_STR("ret_type"));
el_val_t decorator = el_get_field(stmt, EL_STR("decorator"));
el_val_t params_str = js_params_str(params);
js_build_int_names_for_params(params);
if (str_eq(fn_name, EL_STR("main"))) {
js_emit_line(el_str_concat(el_str_concat(EL_STR("function main("), params_str), EL_STR(") {")));
if (str_eq(decorator, EL_STR("async"))) {
js_register_async_fn(fn_name);
if (str_eq(fn_name, EL_STR("main"))) {
js_emit_line(el_str_concat(el_str_concat(EL_STR("async function main("), params_str), EL_STR(") {")));
} else {
js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("async function "), fn_name), EL_STR("(")), params_str), EL_STR(") {")));
}
} else {
js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("function "), fn_name), EL_STR("(")), params_str), EL_STR(") {")));
if (str_eq(fn_name, EL_STR("main"))) {
js_emit_line(el_str_concat(el_str_concat(EL_STR("function main("), params_str), EL_STR(") {")));
} else {
js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("function "), fn_name), EL_STR("(")), params_str), EL_STR(") {")));
}
}
el_val_t decl = native_list_empty();
el_val_t np = native_list_len(params);
@@ -6387,35 +6505,81 @@ el_val_t js_is_top_level_decl(el_val_t stmt) {
}
el_val_t codegen_js(el_val_t stmts, el_val_t source) {
return codegen_js_inner(stmts, source, 0, EL_STR(""));
return 0;
}
el_val_t codegen_js_bundle(el_val_t stmts, el_val_t source, el_val_t runtime_content) {
return codegen_js_inner(stmts, source, 1, runtime_content);
return 0;
}
el_val_t codegen_js_inner(el_val_t stmts, el_val_t source, el_val_t bundle_mode, el_val_t runtime_content) {
state_set(EL_STR("__js_int_names"), EL_STR(""));
state_set(EL_STR("__js_match_counter"), EL_STR(""));
state_set(EL_STR("__js_async_fns"), EL_STR(""));
js_emit_line(EL_STR("// Generated by elc --target=js"));
js_emit_line(EL_STR("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js"));
js_emit_line(EL_STR("import \"./el_runtime.js\";"));
js_emit_line(EL_STR("const {"));
js_emit_line(EL_STR(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,"));
js_emit_line(EL_STR(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,"));
js_emit_line(EL_STR(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,"));
js_emit_line(EL_STR(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,"));
js_emit_line(EL_STR(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,"));
js_emit_line(EL_STR(" list_push, list_join, list_range,"));
js_emit_line(EL_STR(" el_map_new, el_get_field, el_map_get, el_map_set,"));
js_emit_line(EL_STR(" http_get, http_post, http_post_json,"));
js_emit_line(EL_STR(" fs_read, fs_write, fs_list,"));
js_emit_line(EL_STR(" json_parse, json_stringify, json_get, json_get_string, json_get_int,"));
js_emit_line(EL_STR(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,"));
js_emit_line(EL_STR(" el_retain, el_release,"));
js_emit_line(EL_STR(" append, len, get, map_get, map_set,"));
js_emit_line(EL_STR(" native_list_get, native_list_len, native_list_append, native_list_empty,"));
js_emit_line(EL_STR(" native_list_clone, native_string_chars, native_int_to_str,"));
js_emit_line(EL_STR(" args, state_set, state_get, state_del, state_keys, env,"));
js_emit_line(EL_STR(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,"));
js_emit_line(EL_STR(" engram_node, engram_search, engram_activate,"));
js_emit_line(EL_STR(" llm_call, llm_call_system,"));
js_emit_line(EL_STR("} = globalThis.__el;"));
js_emit_blank();
if (bundle_mode) {
js_emit_line(EL_STR("// Bundle mode: runtime inlined, no import statement needed."));
js_emit_line(EL_STR(""));
js_emit_line(EL_STR(";(function() {"));
js_emit_line(EL_STR("\"use strict\";"));
js_emit_line(js_strip_es_exports(runtime_content));
js_emit_line(EL_STR(""));
} else {
js_emit_line(EL_STR("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js"));
js_emit_line(EL_STR("import \"./el_runtime.js\";"));
}
if (!bundle_mode) {
js_emit_line(EL_STR("const {"));
js_emit_line(EL_STR(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,"));
js_emit_line(EL_STR(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,"));
js_emit_line(EL_STR(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,"));
js_emit_line(EL_STR(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,"));
js_emit_line(EL_STR(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,"));
js_emit_line(EL_STR(" list_push, list_join, list_range,"));
js_emit_line(EL_STR(" el_map_new, el_get_field, el_map_get, el_map_set,"));
js_emit_line(EL_STR(" http_get, http_post, http_post_json,"));
js_emit_line(EL_STR(" fs_read, fs_write, fs_list,"));
js_emit_line(EL_STR(" json_parse, json_stringify, json_get, json_get_string, json_get_int,"));
js_emit_line(EL_STR(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,"));
js_emit_line(EL_STR(" el_retain, el_release,"));
js_emit_line(EL_STR(" append, len, get, map_get, map_set,"));
js_emit_line(EL_STR(" native_list_get, native_list_len, native_list_append, native_list_empty,"));
js_emit_line(EL_STR(" native_list_clone, native_string_chars, native_int_to_str,"));
js_emit_line(EL_STR(" args, state_set, state_get, state_del, state_keys, env,"));
js_emit_line(EL_STR(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,"));
js_emit_line(EL_STR(" engram_node, engram_search, engram_activate,"));
js_emit_line(EL_STR(" llm_call, llm_call_system,"));
js_emit_line(EL_STR(" dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,"));
js_emit_line(EL_STR(" dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,"));
js_emit_line(EL_STR(" dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,"));
js_emit_line(EL_STR(" dom_append, dom_remove, dom_is_null,"));
js_emit_line(EL_STR(" dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,"));
js_emit_line(EL_STR(" dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,"));
js_emit_line(EL_STR(" set_timeout, set_interval, clear_interval,"));
js_emit_line(EL_STR(" local_storage_get, local_storage_set, local_storage_remove,"));
js_emit_line(EL_STR(" window_location, window_redirect, window_on_load,"));
js_emit_line(EL_STR(" console_log,"));
js_emit_line(EL_STR(" window_set, window_get, native_js, native_js_call,"));
js_emit_line(EL_STR("} = globalThis.__el;"));
js_emit_blank();
}
el_val_t n = native_list_len(stmts);
el_val_t i = 0;
while (i < n) {
el_val_t stmt = native_list_get(stmts, i);
el_val_t sk = el_get_field(stmt, EL_STR("stmt"));
if (str_eq(sk, EL_STR("FnDef"))) {
el_val_t dec = el_get_field(stmt, EL_STR("decorator"));
if (str_eq(dec, EL_STR("async"))) {
el_val_t aname = el_get_field(stmt, EL_STR("name"));
js_register_async_fn(aname);
}
}
i = (i + 1);
}
i = 0;
while (i < n) {
el_val_t stmt = native_list_get(stmts, i);
if (js_is_fndef(stmt)) {
@@ -6453,10 +6617,37 @@ el_val_t codegen_js(el_val_t stmts, el_val_t source) {
js_emit_blank();
js_emit_line(EL_STR("main();"));
}
if (bundle_mode) {
js_emit_line(EL_STR(""));
js_emit_line(EL_STR("})();"));
}
return EL_STR("");
return 0;
}
el_val_t js_strip_es_exports(el_val_t content) {
el_val_t lines = str_split(content, EL_STR("\n"));
el_val_t n = native_list_len(lines);
el_val_t out = native_list_empty();
el_val_t i = 0;
while (i < n) {
el_val_t line = native_list_get(lines, i);
el_val_t trimmed = str_trim(line);
if (str_starts_with(trimmed, EL_STR("export {"))) {
i = n;
} else {
if (str_starts_with(trimmed, EL_STR("export default"))) {
i = n;
} else {
out = native_list_append(out, line);
}
}
i = (i + 1);
}
return str_join(out, EL_STR("\n"));
return 0;
}
el_val_t compile(el_val_t source) {
el_val_t tokens = lex(source);
el_val_t stmts = parse(tokens);
@@ -6473,6 +6664,19 @@ el_val_t compile_js(el_val_t source) {
return 0;
}
el_val_t compile_js_with_bundle(el_val_t source, el_val_t runtime_path) {
el_val_t tokens = lex(source);
el_val_t stmts = parse(tokens);
el_release(tokens);
el_val_t runtime_content = fs_read(runtime_path);
if (str_eq(runtime_content, EL_STR(""))) {
println(el_str_concat(EL_STR("el-compiler: warning: --bundle: could not read runtime at "), runtime_path));
println(EL_STR("el-compiler: warning: bundle output will be incomplete"));
}
return codegen_js_bundle(stmts, source, runtime_content);
return 0;
}
el_val_t compile_dispatch(el_val_t tgt, el_val_t source) {
if (str_eq(tgt, EL_STR("js"))) {
return compile_js(source);
@@ -6481,6 +6685,14 @@ el_val_t compile_dispatch(el_val_t tgt, el_val_t source) {
return 0;
}
el_val_t compile_dispatch_bundle(el_val_t tgt, el_val_t source, el_val_t runtime_path) {
if (str_eq(tgt, EL_STR("js"))) {
return compile_js_with_bundle(source, runtime_path);
}
return compile(source);
return 0;
}
el_val_t detect_target(el_val_t argv) {
el_val_t n = native_list_len(argv);
el_val_t i = 0;
@@ -6525,6 +6737,127 @@ el_val_t detect_emit_header(el_val_t argv) {
return 0;
}
el_val_t detect_bundle(el_val_t argv) {
el_val_t n = native_list_len(argv);
el_val_t i = 0;
while (i < n) {
el_val_t a = native_list_get(argv, i);
if (str_eq(a, EL_STR("--bundle"))) {
return 1;
}
i = (i + 1);
}
return 0;
return 0;
}
el_val_t detect_minify(el_val_t argv) {
el_val_t n = native_list_len(argv);
el_val_t i = 0;
while (i < n) {
el_val_t a = native_list_get(argv, i);
if (str_eq(a, EL_STR("--minify"))) {
return 1;
}
i = (i + 1);
}
return 0;
return 0;
}
el_val_t detect_obfuscate(el_val_t argv) {
el_val_t n = native_list_len(argv);
el_val_t i = 0;
while (i < n) {
el_val_t a = native_list_get(argv, i);
if (str_eq(a, EL_STR("--obfuscate"))) {
return 1;
}
i = (i + 1);
}
return 0;
return 0;
}
el_val_t make_temp_path(el_val_t suffix) {
el_val_t pid = getpid_now();
el_val_t ts = time_now();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("/tmp/elc-"), native_int_to_str(pid)), EL_STR("-")), native_int_to_str(ts)), EL_STR(".")), suffix);
return 0;
}
el_val_t js_reserved_names(void) {
return EL_STR("neuronDemoToggle,neuronDemoSend,neuronDemoReset,signInWith,signInWithEmail,signUpWithEmail,sendMagicLink,signOut,resetPassword,sendResetEmail,updatePassword,showSignIn,showSignUp,hideReset,setSort,addFamilyMember,removeFamilyMember,copyForPlatform,entHeadcountChange,NEURON_CFG");
return 0;
}
el_val_t find_node_tool(el_val_t tool_name, el_val_t src_dir) {
el_val_t cand1 = el_str_concat(el_str_concat(src_dir, EL_STR("/node_modules/.bin/")), tool_name);
el_val_t check1 = str_trim(exec_capture(el_str_concat(el_str_concat(EL_STR("test -x "), cand1), EL_STR(" && echo yes 2>/dev/null"))));
if (str_eq(check1, EL_STR("yes"))) {
return cand1;
}
el_val_t parent_dir = dirname_of(src_dir);
el_val_t cand2 = el_str_concat(el_str_concat(parent_dir, EL_STR("/node_modules/.bin/")), tool_name);
el_val_t check2 = str_trim(exec_capture(el_str_concat(el_str_concat(EL_STR("test -x "), cand2), EL_STR(" && echo yes 2>/dev/null"))));
if (str_eq(check2, EL_STR("yes"))) {
return cand2;
}
el_val_t npx_path = str_trim(exec_capture(EL_STR("which npx 2>/dev/null")));
if (!str_eq(npx_path, EL_STR(""))) {
return el_str_concat(EL_STR("npx --yes "), tool_name);
}
return EL_STR("");
return 0;
}
el_val_t apply_minify(el_val_t js_path, el_val_t out_path, el_val_t src_dir) {
el_val_t terser = find_node_tool(EL_STR("terser"), src_dir);
if (str_eq(terser, EL_STR(""))) {
println(EL_STR("el-compiler: error: terser not found. Run 'npm install terser' in your project directory."));
return 0;
}
el_val_t names = js_reserved_names();
el_val_t compress_opts = EL_STR("passes=2,drop_console=false,drop_debugger=true");
el_val_t mangle_reserved = el_str_concat(el_str_concat(EL_STR("'reserved=["), names), EL_STR("]'"));
el_val_t cmd = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(terser, EL_STR(" ")), js_path), EL_STR(" --compress ")), compress_opts), EL_STR(" --mangle ")), mangle_reserved), EL_STR(" --output ")), out_path);
el_val_t ret = exec_command(cmd);
if (ret == 0) {
return 1;
}
println(el_str_concat(el_str_concat(EL_STR("el-compiler: error: terser failed (exit "), native_int_to_str(ret)), EL_STR(")")));
return 0;
return 0;
}
el_val_t apply_obfuscate(el_val_t js_path, el_val_t out_path, el_val_t src_dir) {
el_val_t obfuscator = find_node_tool(EL_STR("javascript-obfuscator"), src_dir);
if (str_eq(obfuscator, EL_STR(""))) {
println(EL_STR("el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory."));
return 0;
}
el_val_t names = js_reserved_names();
el_val_t cmd = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(obfuscator, EL_STR(" ")), js_path), EL_STR(" --output ")), out_path), EL_STR(" --compact true --simplify true --string-array true --string-array-encoding base64 --string-array-threshold 0.75 --identifier-names-generator hexadecimal --rename-globals false --self-defending false --reserved-names ")), names);
el_val_t ret = exec_command(cmd);
if (ret == 0) {
return 1;
}
println(el_str_concat(el_str_concat(EL_STR("el-compiler: error: javascript-obfuscator failed (exit "), native_int_to_str(ret)), EL_STR(")")));
return 0;
return 0;
}
el_val_t resolve_runtime_path(el_val_t src_path) {
el_val_t src_dir = dirname_of(src_path);
el_val_t candidate = el_str_concat(src_dir, EL_STR("/el_runtime.js"));
el_val_t existing = fs_read(candidate);
if (!str_eq(existing, EL_STR(""))) {
return candidate;
}
return EL_STR("");
return 0;
}
el_val_t type_node_to_el(el_val_t t) {
el_val_t k = el_get_field(t, EL_STR("kind"));
if (str_eq(k, EL_STR("Simple"))) {
@@ -6547,7 +6880,7 @@ el_val_t emit_header(el_val_t stmts, el_val_t hdr_path) {
el_val_t n = native_list_len(stmts);
el_val_t i = 0;
el_val_t parts = native_list_empty();
parts = native_list_append(parts, EL_STR("// auto-generated by elc --emit-header - do not edit\n"));
parts = native_list_append(parts, EL_STR("// auto-generated by elc --emit-header \xe2\x80\x94 do not edit\n"));
while (i < n) {
el_val_t stmt = native_list_get(stmts, i);
el_val_t kind = el_get_field(stmt, EL_STR("stmt"));
@@ -6661,17 +6994,76 @@ el_val_t resolve_imports(el_val_t src_path) {
return 0;
}
el_val_t run_with_postprocess(el_val_t tgt, el_val_t source, el_val_t src_path, el_val_t do_bundle, el_val_t do_obfuscate, el_val_t argc, el_val_t positional) {
el_val_t src_dir = dirname_of(src_path);
el_val_t tmp_gen = make_temp_path(EL_STR("js"));
el_val_t tmp_min = make_temp_path(EL_STR("min.js"));
stdout_to_file(tmp_gen);
if (do_bundle) {
el_val_t runtime_path = resolve_runtime_path(src_path);
compile_dispatch_bundle(tgt, source, runtime_path);
} else {
compile_dispatch(tgt, source);
}
stdout_restore();
el_val_t ok_min = apply_minify(tmp_gen, tmp_min, src_dir);
if (!ok_min) {
exec_command(el_str_concat(el_str_concat(el_str_concat(EL_STR("rm -f "), tmp_gen), EL_STR(" ")), tmp_min));
exit(1);
}
state_set(EL_STR("__elc_final_js"), tmp_min);
if (do_obfuscate) {
el_val_t tmp_obf = make_temp_path(EL_STR("obf.js"));
el_val_t ok_obf = apply_obfuscate(tmp_min, tmp_obf, src_dir);
if (!ok_obf) {
exec_command(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("rm -f "), tmp_gen), EL_STR(" ")), tmp_min), EL_STR(" ")), tmp_obf));
exit(1);
}
state_set(EL_STR("__elc_final_js"), tmp_obf);
}
el_val_t final_path = state_get(EL_STR("__elc_final_js"));
el_val_t final_js = fs_read(final_path);
exec_command(el_str_concat(el_str_concat(el_str_concat(EL_STR("rm -f "), tmp_gen), EL_STR(" ")), tmp_min));
if (do_obfuscate) {
exec_command(el_str_concat(EL_STR("rm -f "), final_path));
}
if (argc >= 2) {
el_val_t out_path = native_list_get(positional, 1);
el_val_t ok = fs_write(out_path, final_js);
if (ok) {
return 0;
} else {
println(EL_STR("el-compiler: failed to write output"));
exit(1);
}
}
print(final_js);
return 0;
}
int main(int _argc, char** _argv) {
el_runtime_init_args(_argc, _argv);
el_val_t argv = args();
el_val_t tgt = detect_target(argv);
el_val_t do_emit_header = detect_emit_header(argv);
el_val_t do_bundle = detect_bundle(argv);
el_val_t do_minify = detect_minify(argv);
el_val_t do_obfuscate = detect_obfuscate(argv);
if (do_obfuscate) {
do_minify = 1;
}
el_val_t positional = strip_flags(argv);
el_val_t argc = native_list_len(positional);
if (argc < 1) {
println(EL_STR("el-compiler: usage: elc [--target=c|js] [--emit-header] <source.el> [<output>]"));
println(EL_STR("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]"));
exit(1);
}
if (do_minify) {
if (!str_eq(tgt, EL_STR("js"))) {
println(EL_STR("el-compiler: error: --minify and --obfuscate require --target=js"));
exit(1);
}
}
el_val_t src_path = native_list_get(positional, 0);
if (do_emit_header) {
el_val_t raw_source = fs_read(src_path);
@@ -6683,7 +7075,17 @@ int main(int _argc, char** _argv) {
el_release(hdr_stmts);
}
el_val_t source = resolve_imports(src_path);
el_val_t out = compile_dispatch(tgt, source);
if (do_minify) {
run_with_postprocess(tgt, source, src_path, do_bundle, do_obfuscate, argc, positional);
exit(0);
}
el_val_t out = EL_STR("");
if (do_bundle) {
el_val_t runtime_path = resolve_runtime_path(src_path);
out = compile_dispatch_bundle(tgt, source, runtime_path);
} else {
out = compile_dispatch(tgt, source);
}
if (argc >= 2) {
el_val_t out_path = native_list_get(positional, 1);
el_val_t ok = fs_write(out_path, out);
@@ -79,6 +79,8 @@ extern "C" {
void println(el_val_t s);
void print(el_val_t s);
el_val_t readline(void);
el_val_t stdout_to_file(el_val_t path); /* redirect println to a file */
el_val_t stdout_restore(void); /* restore stdout after capture */
/* ── String builtins ─────────────────────────────────────────────────────── */
@@ -212,6 +214,13 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
* where each value is the array of attribute names allowed for that tag. */
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
/* ── HTML template helpers ───────────────────────────────────────────────────
* Used by compiled El HTML template expressions.
* html_escape(s) escape & < > " ' for safe inline interpolation.
* html_raw(s) identity; explicit opt-out from escaping (`raw()` form). */
el_val_t html_escape(el_val_t s);
el_val_t html_raw(el_val_t s);
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t fs_read(el_val_t path);
@@ -601,13 +610,6 @@ el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t d
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
el_val_t engram_stats_json(void);
el_val_t engram_list_layers_json(void);
/* Working memory introspection — count, mean weight, and top-N snapshot.
* Ported from el-compiler/runtime on 2026-06-30 self-review. */
el_val_t engram_wm_count(void);
el_val_t engram_wm_avg_weight(void);
el_val_t engram_wm_top_json(el_val_t n);
/* Merge-load: add nodes/edges from a snapshot without resetting the store. */
el_val_t engram_load_merge(el_val_t path);
/* engram_compile_layered_json — produce a prompt-ready text block split
* into "[LAYER 0 — STRUCTURAL]" (non-suppressible layers, sacred fire)
* and "[ENGRAM CONTEXT]" (standard suppressible layers). Returns "" if
@@ -758,18 +760,6 @@ el_val_t trace_span_start(el_val_t name);
el_val_t trace_span_end(el_val_t span_handle);
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
/* ── Runtime symbols required by the soul modules ──────────────────────────── */
/* All implemented in el_runtime.c but omitted from this release header; the soul dist modules
* reference them directly, so the public header must export them. Declarations only mirrors the
* mainline el_runtime.h and is platform-independent (no behavioural change to the POSIX build). */
typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body);
typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, el_val_t body, el_val_t headers);
el_val_t el_arena_push(void);
el_val_t el_arena_pop(el_val_t mark);
void http_serve_async(el_val_t port, el_val_t handler);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
#ifdef __cplusplus
}
#endif
@@ -128,6 +128,22 @@ function str_pad_right(s, width, pad) {
return String(s).padEnd(width, String(pad));
}
// ── HTML template helpers ────────────────────────────────────────────────────
// Used by compiled El HTML template expressions.
// html_escape(s) — escape & < > " ' for safe inline interpolation.
// html_raw(s) — identity; explicit opt-out from escaping (raw() form).
function html_escape(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function html_raw(s) { return s; }
// ── Math ────────────────────────────────────────────────────────────────────
function el_abs(n) { return Math.abs(n); }
@@ -1017,6 +1033,8 @@ export {
fs_read, fs_write, fs_list,
json_parse, json_stringify, json_get, json_get_string, json_get_int,
time_now, time_now_utc, sleep_ms,
// HTML template helpers
html_escape, html_raw,
bool_to_str, exit_program, args, env,
state_set, state_get, state_del, state_keys,
el_cgi_init,
@@ -1673,7 +1673,6 @@ static void* http_worker_v2(void* arg) {
HttpWorkerArg* a = (HttpWorkerArg*)arg;
int fd = a->fd;
free(a);
int is_sse = 0;
char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL;
if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) {
http_handler4_fn h = http_lookup_active_v2();
@@ -1681,39 +1680,28 @@ static void* http_worker_v2(void* arg) {
int head_only = (method && strcmp(method, "HEAD") == 0);
const char* dispatch_method = head_only ? "GET" : method;
el_request_start(); /* begin per-request arena */
/* Expose the raw fd to El SSE builtins (__http_conn_fd etc.). */
el_seed_set_http_conn_fd(fd);
if (h) {
el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : "");
el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body));
const char* rs = EL_CSTR(r);
/* Detect SSE sentinel — handler took ownership of the fd. */
if (rs && strcmp(rs, "__sse__") == 0) {
is_sse = 1;
} else {
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
response = malloc(rlen + 1);
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
else if (response) { response[0] = '\0'; }
}
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
response = malloc(rlen + 1);
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
else if (response) { response[0] = '\0'; }
el_release(hmap);
} else {
response = el_strdup_persist(
"el-runtime: no v2 http handler registered "
"(call http_set_handler_v2)");
}
el_seed_set_http_conn_fd(-1); /* clear before arena teardown */
el_request_end(); /* free all intermediate strings */
if (!is_sse) {
_tl_http_head_only = head_only;
http_send_response(fd, response);
_tl_http_head_only = 0;
free(response);
}
_tl_http_head_only = head_only;
http_send_response(fd, response);
_tl_http_head_only = 0;
free(response);
}
free(method); free(path); free(body); free(hdr_block);
/* SSE handlers close the fd themselves via __http_sse_close. */
if (!is_sse) close(fd);
close(fd);
pthread_mutex_lock(&_http_conn_mu);
_http_conn_active--;
pthread_cond_signal(&_http_conn_cv);
@@ -176,11 +176,6 @@ void http_set_handler_v2(el_val_t name);
* auto-content-type contract for legacy handlers that return plain bodies. */
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
* The getter is exposed as __http_conn_fd() to El programs. */
void el_seed_set_http_conn_fd(int fd);
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
* 60000ms). Read lazily on first use, so setting the env var any time before
* the first http_* call is sufficient. */
@@ -948,6 +948,10 @@ fn js_cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [St
if kind == "TypeDef" { return declared }
if kind == "EnumDef" { return declared }
if kind == "Import" { return declared }
// TestDef: skip in normal mode; handled by js_codegen_test in test mode.
if kind == "TestDef" { return declared }
// Assert: no-op in normal mode; handled by js_cg_stmt_assert in test mode.
if kind == "Assert" { return declared }
if kind == "TryCatch" {
let try_body = stmt["try_body"]
@@ -1168,20 +1172,178 @@ fn js_is_top_level_decl(stmt: Map<String, Any>) -> Bool {
if kind == "CgiBlock" { return true }
if kind == "ServiceBlock" { return true }
if kind == "ExternFn" { return true }
if kind == "TestDef" { return true }
false
}
// Test mode codegen (JS)
//
// reporter = "text" human-readable output to stderr (console.error)
// reporter = "json" newline-delimited JSON to stdout (process.stdout.write)
//
// The test function returns bool: true = pass, false = fail.
fn js_cg_stmt_assert_text(stmt: Map<String, Any>, test_name: String) -> Void {
let expr_node = stmt["expr"]
let msg: String = stmt["msg"]
let expr_c: String = js_cg_expr(expr_node)
let disp_msg = "assert failed"
if !str_eq(msg, "") { let disp_msg = msg }
js_emit_line(" if (!(" + expr_c + ")) {")
js_emit_line(" process.stderr.write(\" FAIL " + js_escape(test_name) + "" + js_escape(disp_msg) + "\\n\");")
js_emit_line(" return false;")
js_emit_line(" }")
}
fn js_cg_stmt_assert_json(stmt: Map<String, Any>, test_name: String, file_name: String, test_line: Int) -> Void {
let expr_node = stmt["expr"]
let msg: String = stmt["msg"]
let assert_line: Int = stmt["line"]
let expr_c: String = js_cg_expr(expr_node)
let disp_msg = "assert failed"
if !str_eq(msg, "") { let disp_msg = msg }
js_emit_line(" if (!(" + expr_c + ")) {")
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_fail\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",assert_line:" + native_int_to_str(assert_line) + ",message:" + js_str_lit(disp_msg) + "}) + \"\\n\");")
js_emit_line(" return false;")
js_emit_line(" }")
}
// js_cg_stmts_in_test: emit test body, routing Assert to the right handler.
fn js_cg_stmts_in_test(stmts: [Map<String, Any>], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] {
let n: Int = native_list_len(stmts)
let i = 0
let decl = declared
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "Assert") {
if str_eq(reporter, "json") {
js_cg_stmt_assert_json(stmt, test_name, file_name, test_line)
} else {
js_cg_stmt_assert_text(stmt, test_name)
}
} else {
let decl = js_cg_stmt(stmt, indent, decl)
}
let i = i + 1
}
decl
}
// js_cg_test_fn: emit a single async test function.
fn js_cg_test_fn(test_def: Map<String, Any>, idx: Int, reporter: String, file_name: String) -> String {
let fn_name: String = "el_test_" + native_int_to_str(idx)
let test_name: String = test_def["name"]
let test_line: Int = test_def["line"]
let body = test_def["body"]
js_emit_line("async function " + fn_name + "() {")
js_cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line)
js_emit_line(" return true;")
js_emit_line("}")
js_emit_blank()
fn_name
}
// js_codegen_test: emit the test runner (replaces main() when --test active).
// reporter: "text" or "json"
// file_name: basename of the source file (used in JSON output)
fn js_codegen_test(stmts: [Map<String, Any>], reporter: String, file_name: String) -> Void {
// Collect TestDef nodes in order.
let n: Int = native_list_len(stmts)
let test_defs: [Map<String, Any>] = native_list_empty()
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "TestDef") {
let test_defs = native_list_append(test_defs, stmt)
}
let i = i + 1
}
let n_tests: Int = native_list_len(test_defs)
// Emit non-test function definitions (skip fn main and TestDef nodes).
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
if js_is_fndef(stmt) {
let fn_name: String = stmt["name"]
if !str_eq(fn_name, "main") {
js_cg_fn(stmt)
}
}
let i = i + 1
}
// Emit each test function.
let ti = 0
while ti < n_tests {
let test_def = native_list_get(test_defs, ti)
js_cg_test_fn(test_def, ti, reporter, file_name)
let ti = ti + 1
}
// Emit the test runner IIFE.
let test_word = "tests"
if n_tests == 1 { let test_word = "test" }
js_emit_line("(async () => {")
js_emit_line(" let pass = 0; let fail = 0;")
if str_eq(reporter, "json") {
// JSON reporter: suite_start to stdout
js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_start\",file:" + js_str_lit(file_name) + ",total:" + native_int_to_str(n_tests) + "}) + \"\\n\");")
let ti = 0
while ti < n_tests {
let test_def = native_list_get(test_defs, ti)
let test_name: String = test_def["name"]
let test_line: Int = test_def["line"]
let fn_name: String = "el_test_" + native_int_to_str(ti)
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_start\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + "}) + \"\\n\");")
js_emit_line(" if (await " + fn_name + "()) {")
js_emit_line(" pass++;")
js_emit_line(" process.stdout.write(JSON.stringify({type:\"test_pass\",name:" + js_str_lit(test_name) + ",file:" + js_str_lit(file_name) + ",line:" + native_int_to_str(test_line) + ",duration_ms:0}) + \"\\n\");")
js_emit_line(" } else { fail++; }")
let ti = ti + 1
}
js_emit_line(" process.stdout.write(JSON.stringify({type:\"suite_end\",passed:pass,failed:fail}) + \"\\n\");")
} else {
// Text reporter: human-readable to stderr
js_emit_line(" process.stderr.write(\"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");")
let ti = 0
while ti < n_tests {
let test_def = native_list_get(test_defs, ti)
let test_name: String = test_def["name"]
let fn_name: String = "el_test_" + native_int_to_str(ti)
js_emit_line(" process.stderr.write(\" RUN " + js_escape(test_name) + "\\n\");")
js_emit_line(" if (await " + fn_name + "()) { pass++; process.stderr.write(\" PASS " + js_escape(test_name) + "\\n\"); }")
js_emit_line(" else { fail++; }")
let ti = ti + 1
}
js_emit_line(" process.stderr.write(\"\\n\" + pass + \" passed, \" + fail + \" failed\\n\");")
}
js_emit_line(" process.exit(fail > 0 ? 1 : 0);")
js_emit_line("})();")
}
// Entry point
fn codegen_js(stmts: [Map<String, Any>], source: String) -> String {
codegen_js_inner(stmts, source, false, "")
codegen_js_inner(stmts, source, false, "", false, "text", "")
}
// codegen_js_test: emit a JS test binary.
// reporter: "text" or "json"
// file_name: basename of the source file (used in JSON output)
fn codegen_js_test(stmts: [Map<String, Any>], source: String, reporter: String, file_name: String) -> String {
codegen_js_inner(stmts, source, false, "", true, reporter, file_name)
}
fn codegen_js_bundle(stmts: [Map<String, Any>], source: String, runtime_content: String) -> String {
codegen_js_inner(stmts, source, true, runtime_content)
codegen_js_inner(stmts, source, true, runtime_content, false, "text", "")
}
fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool, runtime_content: String) -> String {
fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool, runtime_content: String, test_mode: Bool, reporter: String, file_name: String) -> String {
// Reset per-compile state.
state_set("__js_int_names", "")
state_set("__js_match_counter", "")
@@ -1292,6 +1454,12 @@ fn codegen_js_inner(stmts: [Map<String, Any>], source: String, bundle_mode: Bool
let i = i + 1
}
// Test mode: emit test functions and runner, skip normal program logic.
if test_mode {
js_codegen_test(stmts, reporter, file_name)
return ""
}
// Function definitions
let i = 0
while i < n {
File diff suppressed because it is too large Load Diff
@@ -20,44 +20,18 @@ import "codegen.el"
import "codegen-js.el"
// compile full pipeline (C target): source string -> C source string
// Uses JIT function-at-a-time streaming: parse one decl emit C discard AST.
// Peak memory is O(one function's AST) instead of O(whole program AST).
fn compile(source: String) -> String {
// Top-level arena scope: activates the string arena before lex() so that
// ALL strdup allocations (token strings, sig strings, codegen fragments)
// are tracked and freed on pop. Without this, lex() and scan_fn_sigs()
// run before any push, leaving _tl_arena_active=0 and leaking every
// token string. Also prevents inner pop(mark=0) calls from deactivating
// the arena between per-function scopes.
let top_mark: Any = el_arena_push()
let tokens: [Any] = lex(source)
// Fast pre-scan: collect fn signatures + program kind without building
// full expression ASTs. O(tokens) time, minimal allocation.
let sigs: [Map<String, Any>] = scan_fn_sigs(tokens)
// Stream parse-emit: parse one decl at a time, emit C, discard.
// All output written to stdout via println before pop.
codegen_streaming(tokens, sigs, source)
el_arena_pop(top_mark)
""
}
// compile_test like compile() but sets __test_mode so codegen_streaming
// compiles test { } blocks instead of skipping them, and emits the test
// harness main() instead of the normal int main().
fn compile_test(source: String) -> String {
state_set("__test_mode", "1")
let top_mark: Any = el_arena_push()
let tokens: [Any] = lex(source)
let sigs: [Map<String, Any>] = scan_fn_sigs(tokens)
codegen_streaming(tokens, sigs, source)
el_arena_pop(top_mark)
state_set("__test_mode", "")
""
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
// Token list is no longer needed after parsing release it to free memory
// before codegen allocates its own working data on large source files.
el_release(tokens)
codegen(stmts, source)
}
// compile_js full pipeline (JS target, module mode): source string -> JS source string
fn compile_js(source: String) -> String {
let tokens: [Any] = lex(source)
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
// Token list is no longer needed after parsing release it to free memory.
el_release(tokens)
@@ -67,7 +41,7 @@ fn compile_js(source: String) -> String {
// compile_js_with_bundle JS target in bundle mode.
// Reads el_runtime.js from runtime_path and inlines it inside an IIFE.
fn compile_js_with_bundle(source: String, runtime_path: String) -> String {
let tokens: [Any] = lex(source)
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
el_release(tokens)
let runtime_content: String = fs_read(runtime_path)
@@ -78,6 +52,24 @@ fn compile_js_with_bundle(source: String, runtime_path: String) -> String {
codegen_js_bundle(stmts, source, runtime_content)
}
// compile_test full pipeline (C target, test mode): source -> C test runner.
// reporter: "text" or "json"; file_name: basename of the source file.
fn compile_test(source: String, reporter: String, file_name: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
el_release(tokens)
codegen_with_tests(stmts, source, reporter, file_name)
}
// compile_js_test full pipeline (JS target, test mode): source -> JS test runner.
// reporter: "text" or "json"; file_name: basename of the source file.
fn compile_js_test(source: String, reporter: String, file_name: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
el_release(tokens)
codegen_js_test(stmts, source, reporter, file_name)
}
// compile_dispatch pick a backend based on the requested target.
// tgt = "c" | "js"
// (The parameter is named `tgt` because `target` is a reserved keyword
@@ -88,6 +80,13 @@ fn compile_dispatch(tgt: String, source: String) -> String {
compile(source)
}
// compile_dispatch_test pick test-mode backend.
// reporter: "text" or "json"; file_name: basename of the source file.
fn compile_dispatch_test(tgt: String, source: String, reporter: String, file_name: String) -> String {
if str_eq(tgt, "js") { return compile_js_test(source, reporter, file_name) }
compile_test(source, reporter, file_name)
}
// compile_dispatch_bundle like compile_dispatch but bundle mode for JS.
fn compile_dispatch_bundle(tgt: String, source: String, runtime_path: String) -> String {
if str_eq(tgt, "js") { return compile_js_with_bundle(source, runtime_path) }
@@ -185,6 +184,36 @@ fn detect_test(argv: [String]) -> Bool {
return false
}
// Detect --reporter=<value> flag in argv.
// Returns "json" if --reporter=json, otherwise "text" (default).
fn detect_reporter(argv: [String]) -> String {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_starts_with(a, "--reporter=") {
let v: String = str_slice(a, 11, str_len(a))
return v
}
let i = i + 1
}
return "text"
}
// basename_of extract the filename portion of a path (after last '/').
fn basename_of(path: String) -> String {
let n: Int = str_len(path)
let i: Int = n - 1
while i >= 0 {
let c: String = str_slice(path, i, i + 1)
if str_eq(c, "/") {
return str_slice(path, i + 1, n)
}
let i = i - 1
}
return path
}
// Build a unique temp file path: /tmp/elc-<pid>-<timestamp>.<suffix>
fn make_temp_path(suffix: String) -> String {
let pid: Int = getpid_now()
@@ -287,9 +316,6 @@ fn type_node_to_el(t: Map<String, Any>) -> String {
// emit_header write a .elh file from parsed statements.
// Scans for FnDef nodes and emits 'extern fn' declarations.
// NOTE: This function requires the full AST. Prefer emit_header_from_sigs
// for the --emit-header path it works from a token-level scan without
// building expression ASTs, avoiding OOM on large files.
fn emit_header(stmts: [Map<String, Any>], hdr_path: String) -> Void {
let n: Int = native_list_len(stmts)
let i = 0
@@ -328,32 +354,6 @@ fn emit_header(stmts: [Map<String, Any>], hdr_path: String) -> Void {
let ok: Bool = fs_write(hdr_path, content)
}
// emit_header_from_sigs write a .elh file from pre-scanned El signatures.
// Uses the output of scan_fn_sigs_el() no full AST required.
// Peak memory is O(tokens) rather than O(whole-program AST), which prevents
// OOM on large files with HTML template bodies or deep BinOp chains.
fn emit_header_from_sigs(sigs: [Map<String, Any>], hdr_path: String) -> Void {
let n: Int = native_list_len(sigs)
let i: Int = 0
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "// auto-generated by elc --emit-header — do not edit\n")
while i < n {
let sig = native_list_get(sigs, i)
let kind: String = sig["kind"]
if str_eq(kind, "fn") {
let name: String = sig["name"]
let params_el: String = sig["params_el"]
let ret_el: String = sig["ret_el"]
if str_eq(ret_el, "") { let ret_el = "Any" }
let line: String = "extern fn " + name + "(" + params_el + ") -> " + ret_el
let parts = native_list_append(parts, line + "\n")
}
let i = i + 1
}
let content: String = str_join(parts, "")
let ok: Bool = fs_write(hdr_path, content)
}
// Import resolution
//
// elc supports two forms of import:
@@ -525,7 +525,9 @@ fn run_with_postprocess(tgt: String, source: String, src_path: String, do_bundle
// main CLI entry point.
//
// elc <source.el> # emit C to stdout
// elc --test <source.el> # emit C test runner to stdout
// elc --target=js <source.el> # emit JS (module) to stdout
// elc --target=js --test <source.el> # emit JS test runner to stdout
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
// elc --target=js --bundle --minify <source.el> # emit minified IIFE to stdout
// elc --target=js --bundle --obfuscate <source.el> # emit minified+obfuscated IIFE to stdout
@@ -544,6 +546,7 @@ fn main() -> Void {
let do_minify: Bool = detect_minify(argv)
let do_obfuscate: Bool = detect_obfuscate(argv)
let do_test: Bool = detect_test(argv)
let reporter: String = detect_reporter(argv)
// --obfuscate implies --minify: obfuscating unminified code is pointless.
if do_obfuscate {
let do_minify = true
@@ -551,7 +554,7 @@ fn main() -> Void {
let positional: [String] = strip_flags(argv)
let argc: Int = native_list_len(positional)
if argc < 1 {
println("el-compiler: usage: elc [--target=c|js] [--bundle] [--minify] [--obfuscate] [--emit-header] [--test] <source.el> [<output>]")
println("el-compiler: usage: elc [--target=c|js] [--test] [--reporter=text|json] [--bundle] [--minify] [--obfuscate] [--emit-header] <source.el> [<output>]")
exit(1)
}
@@ -565,23 +568,33 @@ fn main() -> Void {
let src_path: String = native_list_get(positional, 0)
// When --emit-header is requested, lex the source file and do a
// token-level signature scan (no full AST) to write a .elh file.
// This avoids OOM on large files with HTML template bodies or deep
// BinOp chains (e.g. checkout.el) parse() builds O(whole-program AST)
// while scan_fn_sigs_el keeps peak memory at O(tokens).
// When --emit-header is requested, parse the source file directly
// (without inlining imports) and write out a .elh file alongside the .c.
if do_emit_header {
el_mem_check()
let raw_source: String = fs_read(src_path)
let hdr_tokens: [Any] = lex(raw_source)
let hdr_sigs: [Map<String, Any>] = scan_fn_sigs_el(hdr_tokens)
let hdr_tokens: [Map<String, Any>] = lex(raw_source)
let hdr_stmts: [Map<String, Any>] = parse(hdr_tokens)
el_release(hdr_tokens)
let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh"
emit_header_from_sigs(hdr_sigs, hdr_path)
el_release(hdr_sigs)
emit_header(hdr_stmts, hdr_path)
el_release(hdr_stmts)
}
let source: String = resolve_imports(src_path)
let file_name: String = basename_of(src_path)
// --test mode: emit a test runner binary instead of the normal program.
if do_test {
let out: String = compile_dispatch_test(tgt, source, reporter, file_name)
if argc >= 2 {
let out_path: String = native_list_get(positional, 1)
let ok: Bool = fs_write(out_path, out)
if ok { exit(0) }
println("el-compiler: failed to write output")
exit(1)
}
exit(0)
}
// When post-processing (--minify or --obfuscate) is requested, redirect
// stdout to a temp file so codegen output can be captured and piped through
@@ -592,12 +605,6 @@ fn main() -> Void {
exit(0)
}
// --test mode: compile with test harness (C target only).
if do_test {
compile_test(source)
exit(0)
}
// Standard path (no post-processing).
let out: String = ""
if do_bundle {
+763
View File
@@ -0,0 +1,763 @@
// lexer.el el self-hosting lexer
//
// Tokenises an el source string into a list of token maps.
// Each token is a Map<String, Any> with keys:
// "kind" -> String (e.g. "Int", "Ident", "Plus")
// "value" -> String (the raw text of the token)
//
// Entry point: fn lex(source: String) -> [Map<String, Any>]
//
// Uses native_string_chars to split the source into a chars list,
// then indexes it with native_list_get avoids O(N²) string cloning.
// Character helpers
fn lex_is_digit(ch: String) -> Bool {
if ch == "0" { return true }
if ch == "1" { return true }
if ch == "2" { return true }
if ch == "3" { return true }
if ch == "4" { return true }
if ch == "5" { return true }
if ch == "6" { return true }
if ch == "7" { return true }
if ch == "8" { return true }
if ch == "9" { return true }
false
}
fn lex_is_alpha(ch: String) -> Bool {
if ch == "a" { return true }
if ch == "b" { return true }
if ch == "c" { return true }
if ch == "d" { return true }
if ch == "e" { return true }
if ch == "f" { return true }
if ch == "g" { return true }
if ch == "h" { return true }
if ch == "i" { return true }
if ch == "j" { return true }
if ch == "k" { return true }
if ch == "l" { return true }
if ch == "m" { return true }
if ch == "n" { return true }
if ch == "o" { return true }
if ch == "p" { return true }
if ch == "q" { return true }
if ch == "r" { return true }
if ch == "s" { return true }
if ch == "t" { return true }
if ch == "u" { return true }
if ch == "v" { return true }
if ch == "w" { return true }
if ch == "x" { return true }
if ch == "y" { return true }
if ch == "z" { return true }
if ch == "A" { return true }
if ch == "B" { return true }
if ch == "C" { return true }
if ch == "D" { return true }
if ch == "E" { return true }
if ch == "F" { return true }
if ch == "G" { return true }
if ch == "H" { return true }
if ch == "I" { return true }
if ch == "J" { return true }
if ch == "K" { return true }
if ch == "L" { return true }
if ch == "M" { return true }
if ch == "N" { return true }
if ch == "O" { return true }
if ch == "P" { return true }
if ch == "Q" { return true }
if ch == "R" { return true }
if ch == "S" { return true }
if ch == "T" { return true }
if ch == "U" { return true }
if ch == "V" { return true }
if ch == "W" { return true }
if ch == "X" { return true }
if ch == "Y" { return true }
if ch == "Z" { return true }
false
}
fn is_alnum_or_underscore(ch: String) -> Bool {
if lex_is_digit(ch) { return true }
if lex_is_alpha(ch) { return true }
if ch == "_" { return true }
false
}
fn lex_is_whitespace(ch: String) -> Bool {
if ch == " " { return true }
if ch == "\t" { return true }
if ch == "\n" { return true }
if ch == "\r" { return true }
false
}
fn make_tok(kind: String, value: String) -> Map<String, Any> {
let ln_s: String = state_get("__lex_line")
let ln: Int = 1
if !str_eq(ln_s, "") { let ln = str_to_int(ln_s) }
{ "kind": kind, "value": value, "line": ln }
}
// Keyword lookup
fn keyword_kind(word: String) -> String {
if word == "let" { return "Let" }
if word == "fn" { return "Fn" }
if word == "type" { return "Type" }
if word == "enum" { return "Enum" }
if word == "match" { return "Match" }
if word == "return" { return "Return" }
if word == "if" { return "If" }
if word == "else" { return "Else" }
if word == "for" { return "For" }
if word == "in" { return "In" }
if word == "while" { return "While" }
if word == "import" { return "Import" }
if word == "from" { return "From" }
if word == "as" { return "As" }
if word == "with" { return "With" }
if word == "sealed" { return "Sealed" }
if word == "activate" { return "Activate" }
if word == "where" { return "Where" }
if word == "test" { return "Test" }
if word == "seed" { return "Seed" }
if word == "assert" { return "Assert" }
if word == "protocol" { return "Protocol" }
if word == "impl" { return "Impl" }
if word == "retry" { return "Retry" }
if word == "times" { return "Times" }
if word == "fallback" { return "Fallback" }
if word == "reason" { return "Reason" }
if word == "parallel" { return "Parallel" }
if word == "trace" { return "Trace" }
if word == "requires" { return "Requires" }
if word == "deploy" { return "Deploy" }
if word == "to" { return "To" }
if word == "via" { return "Via" }
if word == "target" { return "Target" }
if word == "true" { return "Bool" }
if word == "false" { return "Bool" }
if word == "cgi" { return "Cgi" }
if word == "service" { return "Service" }
if word == "manager" { return "Manager" }
if word == "engine" { return "Engine" }
if word == "accessor" { return "Accessor" }
if word == "vessel" { return "Vessel" }
if word == "extern" { return "Extern" }
if word == "try" { return "Try" }
if word == "catch" { return "Catch" }
""
}
// Scan helpers
// All scan helpers receive the chars list and total length.
// scan_digits advance i while chars[i] is a digit
// Returns { "text": ..., "pos": i }
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
if lex_is_digit(ch) {
let parts = native_list_append(parts, ch)
let i = i + 1
} else {
let running = false
}
}
}
{ "text": str_join(parts, ""), "pos": i }
}
// scan_ident advance i while chars[i] is alphanumeric or underscore
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
if is_alnum_or_underscore(ch) {
let parts = native_list_append(parts, ch)
let i = i + 1
} else {
let running = false
}
}
}
{ "text": str_join(parts, ""), "pos": i }
}
// Code-bearing string detection + comment strip
// Inline JS/CSS literals embedded in El source (e.g. <script></script> blobs
// or stylesheet payloads inside string literals) carry their own line and
// block comments. Those comments leak into the served HTML and reveal build
// notes the visitor should never see. We strip them at the lexer so every
// downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form.
//
// looks_like_code heuristic gate so we only strip strings that actually
// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim.
fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool {
let nchars: [String] = native_string_chars(needle)
let nlen: Int = native_list_len(nchars)
if start + nlen > total { return false }
let i = 0
let matched = true
while i < nlen {
let a: String = native_list_get(chars, start + i)
let b: String = native_list_get(nchars, i)
if a == b { let i = i + 1 } else { let matched = false; let i = nlen }
}
matched
}
fn str_has(s: String, needle: String) -> Bool {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
let i = 0
let found = false
while i < total {
if substr_at(chars, i, total, needle) {
let found = true
let i = total
} else {
let i = i + 1
}
}
found
}
fn looks_like_code(s: String) -> Bool {
if str_has(s, "<script") { return true }
if str_has(s, "<style") { return true }
if str_has(s, "function") {
if str_has(s, ";") { return true }
}
false
}
// strip_code_comments character-by-character walk. Tracks JS string state
// (single, double, backtick) and never strips inside one. Backslash escapes
// inside JS strings consume the next char verbatim. URLs like https:// are
// preserved by checking the previous char before treating // as a line
// comment opener: if the char immediately before '/' is ':', emit the '/'
// literally and advance one position.
fn strip_code_comments(s: String) -> String {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
let out_parts: [String] = native_list_empty()
let i = 0
let in_squote = false
let in_dquote = false
let in_btick = false
let prev = ""
while i < total {
let ch: String = native_list_get(chars, i)
let in_js_string = false
if in_squote { let in_js_string = true }
if in_dquote { let in_js_string = true }
if in_btick { let in_js_string = true }
if in_js_string {
// Backslash escape: consume next char verbatim regardless of which.
if ch == "\\" {
let out_parts = native_list_append(out_parts, ch)
let next_i = i + 1
if next_i < total {
let nc: String = native_list_get(chars, next_i)
let out_parts = native_list_append(out_parts, nc)
let prev = nc
let i = next_i + 1
} else {
let prev = ch
let i = next_i
}
} else {
if in_squote {
if ch == "'" { let in_squote = false }
} else {
if in_dquote {
if ch == "\"" { let in_dquote = false }
} else {
if in_btick {
if ch == "`" { let in_btick = false }
}
}
}
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
}
} else {
// Not in a JS string. Check for comment openers.
let next_i = i + 1
let next_ch = ""
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
}
if ch == "/" {
if next_ch == "/" {
// URL guard: prev char ':' means this is "://", not a comment.
if prev == ":" {
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
// Skip until newline (newline itself is preserved so
// surrounding line counts/structure stay sane).
let i = i + 2
let scanning = true
while scanning {
if i >= total {
let scanning = false
} else {
let lc: String = native_list_get(chars, i)
if lc == "\n" {
let scanning = false
} else {
let i = i + 1
}
}
}
let prev = ""
}
} else {
if next_ch == "*" {
// Skip until matching "*/".
let i = i + 2
let scanning2 = true
while scanning2 {
if i >= total {
let scanning2 = false
} else {
let bc: String = native_list_get(chars, i)
if bc == "*" {
let after = i + 1
if after < total {
let nc2: String = native_list_get(chars, after)
if nc2 == "/" {
let i = after + 1
let scanning2 = false
} else {
let i = i + 1
}
} else {
let i = i + 1
}
} else {
let i = i + 1
}
}
}
let prev = ""
} else {
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
}
}
} else {
// Open a JS string?
if ch == "'" {
let in_squote = true
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
if ch == "\"" {
let in_dquote = true
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
if ch == "`" {
let in_btick = true
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
} else {
let out_parts = native_list_append(out_parts, ch)
let prev = ch
let i = i + 1
}
}
}
}
}
}
str_join(out_parts, "")
}
// scan_string scan a quoted string literal, handling \" escapes.
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
let i = start
let parts: [String] = native_list_empty()
let running = true
while running {
if i >= total {
let running = false
} else {
let ch: String = native_list_get(chars, i)
if ch == "\\" {
// escape: peek next char
let next_i = i + 1
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
if next_ch == "\"" {
let parts = native_list_append(parts, "\"")
let i = next_i + 1
} else {
if next_ch == "n" {
let parts = native_list_append(parts, "\n")
let i = next_i + 1
} else {
if next_ch == "t" {
let parts = native_list_append(parts, "\t")
let i = next_i + 1
} else {
if next_ch == "r" {
let parts = native_list_append(parts, "\r")
let i = next_i + 1
} else {
if next_ch == "\\" {
let parts = native_list_append(parts, "\\")
let i = next_i + 1
} else {
let parts = native_list_append(parts, next_ch)
let i = next_i + 1
}
}
}
}
}
} else {
let i = i + 1
}
} else {
if ch == "\"" {
let i = i + 1
let running = false
} else {
let parts = native_list_append(parts, ch)
let i = i + 1
}
}
}
}
{ "text": str_join(parts, ""), "pos": i }
}
// Main lexer
fn lex(source: String) -> [Map<String, Any>] {
let chars: [String] = native_string_chars(source)
let total: Int = native_list_len(chars)
let tokens: [Map<String, Any>] = native_list_empty()
let i: Int = 0
let line_num: Int = 1
state_set("__lex_line", "1")
while i < total {
let ch: String = native_list_get(chars, i)
// Skip whitespace; track newlines for line-number reporting
if lex_is_whitespace(ch) {
if ch == "\n" {
let line_num = line_num + 1
state_set("__lex_line", native_int_to_str(line_num))
}
let i = i + 1
} else {
// Line comments: //
if ch == "/" {
let next_i = i + 1
if next_i < total {
let next_ch: String = native_list_get(chars, next_i)
if next_ch == "/" {
// skip to end of line
let i = i + 2
let running2 = true
while running2 {
if i >= total {
let running2 = false
} else {
let lch: String = native_list_get(chars, i)
if lch == "\n" {
let running2 = false
} else {
let i = i + 1
}
}
}
} else {
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
let i = i + 1
}
} else {
let tokens = native_list_append(tokens, make_tok("Slash", "/"))
let i = i + 1
}
} else {
// String literal
if ch == "\"" {
let result = scan_string(chars, i + 1, total)
let str_text: String = result["text"]
let new_pos: Int = result["pos"]
// Compile-time scrub: strings that embed JS or CSS get
// their // line comments and /* block comments stripped
// before the token reaches the parser. Plain prose passes
// through untouched.
let clean_text = str_text
if looks_like_code(str_text) {
let clean_text = strip_code_comments(str_text)
}
let tokens = native_list_append(tokens, make_tok("Str", clean_text))
let i = new_pos
} else {
// Number literal
if lex_is_digit(ch) {
let result = scan_digits(chars, i, total)
let num_text: String = result["text"]
let new_pos: Int = result["pos"]
// check for float (dot followed by digit)
if new_pos < total {
let dot_ch: String = native_list_get(chars, new_pos)
if dot_ch == "." {
let after_dot = new_pos + 1
if after_dot < total {
let after_dot_ch: String = native_list_get(chars, after_dot)
if lex_is_digit(after_dot_ch) {
let frac_result = scan_digits(chars, after_dot, total)
let frac_text: String = frac_result["text"]
let frac_pos: Int = frac_result["pos"]
let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text))
let i = frac_pos
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let i = new_pos
}
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let i = new_pos
}
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let i = new_pos
}
} else {
let tokens = native_list_append(tokens, make_tok("Int", num_text))
let i = new_pos
}
} else {
// Identifier or keyword
if lex_is_alpha(ch) || ch == "_" {
let result = scan_ident(chars, i, total)
let word: String = result["text"]
let new_pos: Int = result["pos"]
let kw = keyword_kind(word)
if kw == "" {
let tokens = native_list_append(tokens, make_tok("Ident", word))
} else {
let tokens = native_list_append(tokens, make_tok(kw, word))
}
let i = new_pos
} else {
// Multi-char and single-char operators/delimiters
let peek_i = i + 1
let peek_ch = ""
if peek_i < total {
let peek_ch: String = native_list_get(chars, peek_i)
}
if ch == "=" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("EqEq", "=="))
let i = i + 2
} else {
if peek_ch == ">" {
let tokens = native_list_append(tokens, make_tok("FatArrow", "=>"))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Eq", "="))
let i = i + 1
}
}
} else {
if ch == "!" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("NotEq", "!="))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Not", "!"))
let i = i + 1
}
} else {
if ch == "<" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("LtEq", "<="))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Lt", "<"))
let i = i + 1
}
} else {
if ch == ">" {
if peek_ch == "=" {
let tokens = native_list_append(tokens, make_tok("GtEq", ">="))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Gt", ">"))
let i = i + 1
}
} else {
if ch == "&" {
if peek_ch == "&" {
let tokens = native_list_append(tokens, make_tok("And", "&&"))
let i = i + 2
} else {
let i = i + 1
}
} else {
if ch == "|" {
if peek_ch == "|" {
let tokens = native_list_append(tokens, make_tok("Or", "||"))
let i = i + 2
} else {
if peek_ch == ">" {
let tokens = native_list_append(tokens, make_tok("PipeOp", "|>"))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Pipe", "|"))
let i = i + 1
}
}
} else {
if ch == "-" {
if peek_ch == ">" {
let tokens = native_list_append(tokens, make_tok("Arrow", "->"))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Minus", "-"))
let i = i + 1
}
} else {
if ch == ":" {
if peek_ch == ":" {
let tokens = native_list_append(tokens, make_tok("ColonColon", "::"))
let i = i + 2
} else {
let tokens = native_list_append(tokens, make_tok("Colon", ":"))
let i = i + 1
}
} else {
if ch == "+" {
let tokens = native_list_append(tokens, make_tok("Plus", "+"))
let i = i + 1
} else {
if ch == "*" {
let tokens = native_list_append(tokens, make_tok("Star", "*"))
let i = i + 1
} else {
if ch == "%" {
let tokens = native_list_append(tokens, make_tok("Percent", "%"))
let i = i + 1
} else {
if ch == "(" {
let tokens = native_list_append(tokens, make_tok("LParen", "("))
let i = i + 1
} else {
if ch == ")" {
let tokens = native_list_append(tokens, make_tok("RParen", ")"))
let i = i + 1
} else {
if ch == "{" {
let tokens = native_list_append(tokens, make_tok("LBrace", "{"))
let i = i + 1
} else {
if ch == "}" {
let tokens = native_list_append(tokens, make_tok("RBrace", "}"))
let i = i + 1
} else {
if ch == "[" {
let tokens = native_list_append(tokens, make_tok("LBracket", "["))
let i = i + 1
} else {
if ch == "]" {
let tokens = native_list_append(tokens, make_tok("RBracket", "]"))
let i = i + 1
} else {
if ch == "," {
let tokens = native_list_append(tokens, make_tok("Comma", ","))
let i = i + 1
} else {
if ch == "." {
let tokens = native_list_append(tokens, make_tok("Dot", "."))
let i = i + 1
} else {
if ch == ";" {
let tokens = native_list_append(tokens, make_tok("Semicolon", ";"))
let i = i + 1
} else {
if ch == "@" {
let tokens = native_list_append(tokens, make_tok("At", "@"))
let i = i + 1
} else {
if ch == "?" {
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
let i = i + 1
} else {
if ch == "#" {
let tokens = native_list_append(tokens, make_tok("Hash", "#"))
let i = i + 1
} else {
// unknown char skip
let i = i + 1
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
let tokens = native_list_append(tokens, make_tok("Eof", ""))
tokens
}
File diff suppressed because it is too large Load Diff
+5 -79
View File
@@ -77,33 +77,6 @@ fn parse_manifest_entry(src: String) -> String {
return ""
}
// parse_manifest_c_sources - collect all `c_source "path"` lines from the
// build block. Returns a flat list of path strings.
fn parse_manifest_c_sources(src: String) -> [String] {
let result: [String] = native_list_empty()
let lines: [String] = str_split(src, "\n")
let n: Int = native_list_len(lines)
let i = 0
while i < n {
let line: String = native_list_get(lines, i)
let t: String = str_trim(line)
if str_starts_with(t, "c_source ") {
let after: String = str_slice(t, 9, str_len(t))
let trimmed: String = str_trim(after)
if str_starts_with(trimmed, "\"") {
let inner: String = str_slice(trimmed, 1, str_len(trimmed))
let q: Int = str_index_of(inner, "\"")
if q >= 0 {
let path: String = str_slice(inner, 0, q)
let result = native_list_append(result, path)
}
}
}
let i = i + 1
}
return result
}
fn parse_manifest_name(src: String) -> String {
let lines: [String] = str_split(src, "\n")
let n: Int = native_list_len(lines)
@@ -252,7 +225,6 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B
let bname: String = basename_noext(src_path)
let c_out: String = out_dir + "/" + bname + ".c"
let elh_out: String = out_dir + "/" + bname + ".elh"
let err_tmp: String = "/tmp/elb-err-" + bname + ".txt"
// Check if recompile needed
if !file_is_newer(src_path, c_out) {
@@ -262,26 +234,18 @@ fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: B
return true
}
// elc streams C to stdout; redirect stderr to a temp file so we can
// surface the actual error message on failure instead of swallowing it.
let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>" + err_tmp
// elc streams C to stdout (collect mode not yet implemented); use
// shell redirection so the output lands in the file, not the terminal.
let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>&1"
println(" compile " + src_path)
if dry_run { return true }
let ret: Int = exec_command(cmd)
if ret != 0 {
// Surface the actual compiler error from stderr
let err_msg: String = str_trim(fs_read(err_tmp))
if !str_eq(err_msg, "") {
println(err_msg)
}
// Remove partial output so a retry starts clean
exec_command("rm -f " + c_out + " " + err_tmp)
println("elb: compile failed: " + src_path)
return false
}
exec_command("rm -f " + err_tmp)
// Move the generated .elh (written next to the source by elc) into
// out_dir so that #include "module.elh" lines in the generated .c
@@ -298,21 +262,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
let parts: [String] = native_list_empty()
// Include both the runtime dir (for el_runtime.h) and the output dir
// (for module.elh cross-module forward declarations).
// Detect clang vs gcc: -fbracket-depth is clang-only; silently ignored
// if unsupported but gcc rejects it with an error.
let bracket_flag: String = "$(cc --version 2>&1 | grep -q clang && printf -- '-fbracket-depth=1024' || true)"
// On macOS, OpenSSL is not on the default linker path. Detect homebrew
// prefix and add it if present (no-op on Linux where libssl is in /usr/lib).
let ossl_lib_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-L{}/lib' 2>/dev/null || true)"
let ossl_inc_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-I{}/include' 2>/dev/null || true)"
// Force-include the C-level master declarations header so every translation
// unit sees all cross-module function signatures. Handles packages (like ELP)
// where modules call each other without explicit El import statements.
// The header is generated by elb --gen-decls or manually placed in out_dir.
let master_decls: String = out_dir + "/elp-c-decls.h"
let has_master: String = str_trim(exec_capture("test -f " + master_decls + " && echo yes || echo no"))
let include_flag: String = if str_eq(has_master, "yes") { "-include " + master_decls } else { "" }
let parts = native_list_append(parts, "cc -O2 " + bracket_flag + " " + ossl_inc_flag + " " + include_flag + " -I " + dirname_of(runtime_path) + " -I " + out_dir)
let parts = native_list_append(parts, "cc -O2 -I " + dirname_of(runtime_path) + " -I " + out_dir)
let i = 0
while i < n {
let f: String = native_list_get(c_files, i)
@@ -320,7 +270,7 @@ fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir
let i = i + 1
}
let parts = native_list_append(parts, runtime_path)
let parts = native_list_append(parts, ossl_lib_flag + " -lcurl -lssl -lcrypto -lpthread -lm")
let parts = native_list_append(parts, "-lcurl -lpthread")
let parts = native_list_append(parts, "-o " + out_bin)
let cmd: String = str_join(parts, " ")
println(" link " + out_bin)
@@ -353,7 +303,6 @@ fn main() -> Void {
let pkg_name: String = parse_manifest_name(manifest_src)
let entry: String = parse_manifest_entry(manifest_src)
let extra_c: [String] = parse_manifest_c_sources(manifest_src)
if str_eq(entry, "") {
println("elb: manifest.el has no 'entry' declaration")
exit(1)
@@ -371,20 +320,6 @@ fn main() -> Void {
runtime_path = elc_dir + "/../el-compiler/runtime/el_runtime.c"
}
}
// If --runtime points to a directory, auto-locate el_runtime.c inside it.
// This lets both forms work:
// --runtime=/opt/el/el-compiler/runtime (directory form)
// --runtime=/opt/el/el-compiler/runtime/el_runtime.c (file form)
if !str_eq(runtime_path, "") {
let is_dir: String = str_trim(exec_capture("test -d " + runtime_path + " && echo dir || echo file"))
if str_eq(is_dir, "dir") {
let candidate: String = runtime_path + "/el_runtime.c"
let has_file: String = str_trim(exec_capture("test -f " + candidate + " && echo yes || echo no"))
if str_eq(has_file, "yes") {
let runtime_path = candidate
}
}
}
if str_eq(runtime_path, "") {
println("elb: cannot locate el_runtime.c - use --runtime=PATH")
exit(1)
@@ -432,15 +367,6 @@ fn main() -> Void {
exit(1)
}
// Append any extra C sources declared in the manifest (e.g. platform stubs)
let ei = 0
let en: Int = native_list_len(extra_c)
while ei < en {
let ec: String = native_list_get(extra_c, ei)
let c_files = native_list_append(c_files, ec)
let ei = ei + 1
}
// Link
let out_bin: String = out_dir + "/" + pkg_name
let linked: Bool = link_binary(c_files, out_bin, runtime_path, out_dir, dry_run)
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -1,21 +0,0 @@
# Compiled El bytecode
*.elc
# C codegen output
*.c
*.o
*.a
*.so
*.dylib
# Combined build artifacts
_combined.el
*-combined.el
# Distribution / build output
dist/
build/
out/
# OS
.DS_Store
-85
View File
@@ -1,85 +0,0 @@
package "elp" {
version "0.7.0"
description "Engram Language Protocol — bidirectional engine mapping between Engram semantic forms and natural language surface text. 31 languages."
edition "2026"
}
build {
entry "src/elp.el"
// Compilation order (dependency order):
// language-profile (no deps)
// vocabulary (no deps)
// morphology (depends on: language-profile)
// morphology-es (depends on: morphology) Spanish
// morphology-fr (depends on: morphology) French
// morphology-de (depends on: morphology) German
// morphology-ru (depends on: morphology) Russian
// morphology-ja (depends on: morphology) Japanese
// morphology-fi (depends on: morphology) Finnish
// morphology-ar (depends on: morphology) Arabic
// morphology-hi (depends on: morphology) Hindi
// morphology-sw (depends on: morphology) Swahili
// morphology-la (depends on: morphology) Latin
// morphology-he (depends on: morphology) Hebrew
// morphology-grc (depends on: morphology) Ancient Greek
// morphology-ang (depends on: morphology) Old English
// morphology-sa (depends on: morphology) Sanskrit
// morphology-got (depends on: morphology) Gothic
// morphology-non (depends on: morphology) Old Norse
// morphology-enm (depends on: morphology) Middle English
// morphology-pi (depends on: morphology) Pali
// morphology-fro (depends on: morphology) Old French
// morphology-goh (depends on: morphology) Old High German
// morphology-sga (depends on: morphology) Old Irish
// morphology-txb (depends on: morphology) Tocharian B
// morphology-peo (depends on: morphology) Old Persian
// morphology-akk (depends on: morphology) Akkadian
// morphology-uga (depends on: morphology) Ugaritic
// morphology-egy (depends on: morphology) Ancient Egyptian
// morphology-sux (depends on: morphology) Sumerian
// morphology-gez (depends on: morphology) Ge'ez (Classical Ethiopic)
// morphology-cop (depends on: morphology) Coptic (Sahidic)
// grammar (depends on: language-profile)
// realizer (depends on: morphology, grammar, language-profile)
// semantics (depends on: grammar, realizer, language-profile)
// elp (depends on: semantics, realizer)
sources [
"src/language-profile.el",
"src/vocabulary.el",
"src/morphology.el",
"src/morphology-es.el",
"src/morphology-fr.el",
"src/morphology-de.el",
"src/morphology-ru.el",
"src/morphology-ja.el",
"src/morphology-fi.el",
"src/morphology-ar.el",
"src/morphology-hi.el",
"src/morphology-sw.el",
"src/morphology-la.el",
"src/morphology-he.el",
"src/morphology-grc.el",
"src/morphology-ang.el",
"src/morphology-sa.el",
"src/morphology-got.el",
"src/morphology-non.el",
"src/morphology-enm.el",
"src/morphology-pi.el",
"src/morphology-fro.el",
"src/morphology-goh.el",
"src/morphology-sga.el",
"src/morphology-txb.el",
"src/morphology-peo.el",
"src/morphology-akk.el",
"src/morphology-uga.el",
"src/morphology-egy.el",
"src/morphology-sux.el",
"src/morphology-gez.el",
"src/morphology-cop.el",
"src/grammar.el",
"src/realizer.el",
"src/semantics.el",
"src/elp.el",
]
}
-91
View File
@@ -1,91 +0,0 @@
> **STATUS: STAGING / PROOF-OF-SHAPE — not the deliverable.** This Python package
> proved the architecture end-to-end against the proven realizer faculty (faithful
> md/docx/midi from real geometry: 0 ungrounded claims, SACRED polarity). Per Will's
> steer, the DELIVERABLE is NATIVE: the seam lives on the existing EL realizer as
> **surface-as-profile** — see `../src/surface-profile.el` and
> `../tests/examples/surface-profile-demo.el` (compiles + runs through elc → C →
> binary). The concepts below (one geometry-carrying frame; surface = a pluggable
> profile; plan/realize; deterministic-from-meaning) are exactly what the native
> module implements. Keep this package as the validated proof; build native.
# Efferent Multimodal Projector
**geometry → any surface, faithfully.** Neuron's own document-generation faculty:
the efferent twin of the ingest organ. Ingest is afferent (world → geometry);
this is efferent (geometry → an arbitrary-format document / any modality).
Built against the **proven** realizer faculty (neuron-talk sidecar `:8756`,
artifact `art-7affa557`). The live soul (`:8742` / `:7770`) is contacted **only**
through the read-only, GET-only `engram_client` — never mutated.
## The pipeline (surface-agnostic)
```
geometry region + surface/format spec
→ PLAN (manifold → document skeleton/DAG; the geometry IS the outline) plan.py
→ REALIZE (proven realizer, scaled sentence → passage, each section faithful) realize.py
→ COHERE (document-level flow / transitions, not stitched sentences) cohere.py
→ EMIT (pluggable SurfaceProjector → the target surface) projectors/
```
**The surface is a PARAMETER.** `pipeline.build_ir(...)` builds ONE
surface-neutral `DocumentIR` (`document_ir.py`); `pipeline.emit(doc, surface)`
projects it to whichever surface you name. Markdown, docx, and MIDI are the same
IR emitted three ways.
## The pivot: a geometry-carrying IR
`DocumentIR` is **not** a text tree. Every `Block` carries BOTH:
- `.sentences` — realized faithful text (what **text** projectors read),
- `.provenance` — the source geometry: `subj_id / relation / obj / polarity /
confidence / importance / salience / node_id` (what **music / image / video**
projectors read).
That single decision is what makes the projector multimodal: text renders the
words; music/image decode the geometry. A claim with no provenance cannot exist
in the IR — faithfulness is structural.
## The one shared seam
`projectors/base.py` — `SurfaceProjector.project(frame: DocumentIR) -> bytes`
(+ `surface / media_type / ext / modality / profile`). Register with
`register()`. Adding a surface changes nothing upstream.
`TwoStageProjector` blesses the peer plan/realize decomposition:
`spec = plan(frame)`, `bytes = realize(spec)`, `project = realize∘plan`; the
`profile` is the pluggable per-surface knob (text lang-profile, music
instr/mode-profile). `projectors/midi.py` is the reference two-stage impl.
## Surfaces
| surface | modality | status | emitter |
|---|---|---|---|
| `markdown` | text | landed | own (str) |
| `docx` | text | landed | own minimal OOXML (stdlib `zipfile`+XML, no lib) |
| `midi` | audio | landed (symbolic-music proof) | own minimal SMF (stdlib `struct`, no lib) |
| `audio` (WAV) | audio | peer agent (additive synth) | conforms to `TwoStageProjector` |
| `image` | image | documented seam | `projectors/seams.py` |
| `video` | video | documented seam (image×sound×time) | `projectors/seams.py` |
Music maps: relation → scale degree (same relation → same pitch), **polarity →
major/minor third (SACRED negation is audible)**, confidence → duration,
importance → velocity, section → register. Deterministic projection from meaning
— nothing invented.
## Faithfulness
`provenance.py` audits the IR: **zero** ungrounded claims, SACRED polarity
preserved (negations reported, never dropped), COHERE introduces no new geometry
(connectives are marked). `trace_table()` emits the geometry → section → claim
table.
## Run
```bash
PY=~/Desktop/lang-realizers/venv/bin/python
PYTHONPATH=~/Desktop/neuron-talk:~/Desktop/lang-realizers $PY generate.py
# writes ./out/{neuron-self,engram-temporal}.{md,docx,mid} + *.audit.json + *.provenance.md
```
Requires the proven realizer env (spaCy + the neuron-talk/lang-realizers engine)
and the read-only engram at `:8742`.
-79
View File
@@ -1,79 +0,0 @@
"""cohere.py — COHERE stage: document-level flow, not stitched sentences.
Fidelity is REALIZE's job; FLOW is this stage's. The hard part beyond sentence
fidelity is that a document must read as one thing. We add connective tissue at
the passage level:
* an opening abstract that names what the document covers (built ONLY from the
section headings that already exist — it introduces no new claim),
* a short transition lead into each section after the first, drawn from a
fixed set of discourse connectives ("Beyond that,", "Relatedly,", ...) that
carry no propositional content,
* ordering so the highest-grounded section leads.
CRITICAL: every connective is marked ``kind="connective"`` in its provenance, so
the faithfulness audit can prove COHERE introduced ZERO new geometry claims. A
transition is discourse glue, never a fact.
"""
from __future__ import annotations
from document_ir import Block, DocumentIR, Provenance
# discourse connectives — pure flow, no propositional content
_TRANSITIONS = [
"Beyond that,", "Relatedly,", "In the same region,", "From there,",
"Alongside this,", "Further,", "Turning to the next facet,",
]
def _connective_prov() -> Provenance:
return Provenance(subj_id=None, subject=None, relation="", obj=None,
polarity="aff", confidence=1.0, node_id=None,
kind="connective")
def _abstract_block(doc: DocumentIR) -> Block:
"""A grounded opening: names the sections, asserts nothing new."""
headings = [s.heading for s in doc.sections]
if not headings:
return Block(role="lead")
if len(headings) == 1:
body = f"This document, generated from Neuron's geometry, covers {headings[0]}."
else:
listed = ", ".join(headings[:-1]) + f", and {headings[-1]}"
body = ("This document is projected directly from Neuron's meaning-geometry. "
f"It traces {listed}.")
b = Block(role="lead")
b.sentences.append(body)
b.provenance.append(_connective_prov())
return b
def cohere_document(doc: DocumentIR, *, add_abstract: bool = True,
add_transitions: bool = True) -> DocumentIR:
"""Order sections by grounding, add abstract + transitions (flow only)."""
# order: strongest-grounded section (mean confidence x #claims) first,
# but keep an explicitly-first section if the plan pinned one via level 1.
def _score(sec):
provs = [p for p in sec.all_provenance() if p.kind == "fact"]
if not provs:
return 0.0
mean_conf = sum(p.confidence for p in provs) / len(provs)
return mean_conf * len(provs)
doc.sections.sort(key=_score, reverse=True)
if add_transitions:
for i, sec in enumerate(doc.sections):
if i == 0 or not sec.blocks:
continue
lead = _TRANSITIONS[(i - 1) % len(_TRANSITIONS)]
first = sec.blocks[0]
if first.sentences:
# prepend the connective to the first sentence (flow, no new claim)
first.sentences[0] = f"{lead} {first.sentences[0][0].lower()}{first.sentences[0][1:]}"
if add_abstract:
doc.meta["abstract"] = _abstract_block(doc)
return doc
-111
View File
@@ -1,111 +0,0 @@
"""document_ir.py — the surface-neutral, GEOMETRY-CARRYING document intermediate.
This is the pivot of the whole efferent projector. A DocumentIR is NOT a text
tree. It is a projection of a meaning-geometry region that carries, at every
leaf, BOTH:
* the realized surface text (``Block.sentences``) — what a TEXT projector reads,
* the source geometry (``Block.provenance``) — what a MUSIC / IMAGE /
VIDEO projector reads.
Because the IR holds the geometry, not just the words, the SAME
plan -> realize -> cohere pipeline drives every surface. A markdown projector
renders the sentences; a music projector reads the provenance edges (salience,
importance, polarity, relation) and maps them onto a symbolic-music surface;
an image/video projector (documented seam) would read the same geometry.
Nothing in this module invents content. Every :class:`Provenance` points at a
real engram node id and a real relation. That is the faithfulness contract made
structural: a claim with no provenance cannot exist in the IR.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
# --------------------------------------------------------------------------- #
# Provenance — the geometry an emitted claim traces to. FAITHFULNESS is here.
# --------------------------------------------------------------------------- #
@dataclass
class Provenance:
"""One geometry edge behind one realized claim.
``kind`` distinguishes a FACT (a structural edge asserted by the geometry,
spoken as fact) from an INTERPRETATION (something attributed, spoken with
attribution) — the facts-as-facts + interpretations-attributed discipline
(memory 80927e26). ``polarity`` is SACRED: a negated edge stays negated.
"""
subj_id: str | None # source engram node id of the subject
subject: str | None # normalized subject surface
relation: str # predicate lemma (e.g. "use", "contain", "be")
obj: str | None # normalized object / complement surface
polarity: str = "aff" # "aff" | "neg" (SACRED — never silently flipped)
confidence: float = 0.0 # extraction confidence in [0,1]
node_id: str | None = None # engram node the claim was extracted from
kind: str = "fact" # "fact" | "interpretation"
importance: float = 0.0 # source node importance (drives music/emphasis)
salience: float = 0.0 # source node salience
def trace(self) -> str:
arrow = "-->" if self.polarity == "aff" else "--NOT-->"
return (f"[{(self.node_id or '?')[:8]}] {self.subject!r} {arrow}"
f"{self.relation} {self.obj!r} (conf {self.confidence:.2f})")
@dataclass
class Block:
"""A passage: one or more faithful sentences + the geometry they trace to.
``sentences`` and ``provenance`` are index-aligned where possible: sentence
``i`` was realized from ``provenance[i]``. A COHERE transition sentence with
no new geometry carries a provenance whose ``kind == "connective"`` so the
audit can see it introduced no new claim.
"""
sentences: list[str] = field(default_factory=list)
provenance: list[Provenance] = field(default_factory=list)
role: str = "body" # "body" | "lead" | "transition"
def text(self) -> str:
return " ".join(s.rstrip(". ") + "." for s in self.sentences if s.strip())
@dataclass
class Section:
heading: str
level: int = 2 # markdown heading level / outline depth
blocks: list[Block] = field(default_factory=list)
seed_ids: list[str] = field(default_factory=list) # geometry nodes of section
summary: str = "" # one-line grounded gloss (for pptx bullets / TOC)
def all_provenance(self) -> list[Provenance]:
out: list[Provenance] = []
for b in self.blocks:
out.extend(b.provenance)
return out
@dataclass
class DocumentIR:
"""The surface-neutral document. Built ONCE, projected to ANY surface."""
title: str
subtitle: str = ""
sections: list[Section] = field(default_factory=list)
seed_id: str | None = None # the geometry region root
format_spec: dict[str, Any] = field(default_factory=dict) # requested shape
meta: dict[str, Any] = field(default_factory=dict)
# -- geometry facets (what non-text projectors consume) ----------------- #
def all_provenance(self) -> list[Provenance]:
out: list[Provenance] = []
for s in self.sections:
out.extend(s.all_provenance())
return out
def claim_count(self) -> int:
return sum(1 for p in self.all_provenance() if p.kind in ("fact", "interpretation"))
def ungrounded_count(self) -> int:
"""Claims with no traceable node — MUST be zero for a faithful doc."""
return sum(1 for p in self.all_provenance()
if p.kind in ("fact", "interpretation") and not p.node_id)
-81
View File
@@ -1,81 +0,0 @@
"""generate.py — drive the projector: one geometry region -> many surfaces.
Proves the thesis with REAL output: builds ONE surface-neutral DocumentIR from
Neuron's OWN self-geometry (read-only against the live soul via the proven
faculty), then EMITS it to Markdown, docx, and MIDI — the same plan/realize/
cohere, three surfaces. Writes the files + the faithfulness audit to ./out/.
"""
from __future__ import annotations
import json
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
import pipeline # noqa: E402
import provenance # noqa: E402
from geometry import load_self_region # noqa: E402
OUT = os.path.join(_HERE, "out")
def _emit_all(doc, stem):
"""Emit one IR to every text/audio surface + audit + provenance."""
for surface in ("markdown", "docx", "midi"):
data = pipeline.emit(doc, surface)
proj = pipeline.get_projector(surface)
path = os.path.join(OUT, f"{stem}.{proj.ext}")
with open(path, "wb") as f:
f.write(data)
print(f" emitted {surface:9s} -> {os.path.basename(path)} ({len(data)} bytes)")
a = provenance.audit(doc)
with open(os.path.join(OUT, f"{stem}.audit.json"), "w") as f:
json.dump(a, f, indent=2)
with open(os.path.join(OUT, f"{stem}.provenance.md"), "w") as f:
f.write(provenance.trace_table(doc))
print(" audit:", {k: a[k] for k in ("claims", "ungrounded_claims",
"negations_preserved", "distinct_source_nodes", "faithful")})
return a
def main():
os.makedirs(OUT, exist_ok=True)
print("surfaces registered:", pipeline.available_surfaces())
# ---- Document 1: Neuron's self-description (marquee) ------------------- #
print("\n[1] Neuron self-description")
region = load_self_region(max_nodes=9)
print(" self region:", region)
doc1 = pipeline.build_ir(
None, region=region,
title="Neuron: A Self-Description from Its Own Geometry",
subtitle="Projected efferently from the engram — every claim traces a node.",
format_spec={"genre": "self-description", "register": "expository"},
max_sections=5, conf_floor=0.6)
print(f" IR: {len(doc1.sections)} sections, {doc1.claim_count()} claims, "
f"ungrounded={doc1.ungrounded_count()}")
_emit_all(doc1, "neuron-self")
# ---- Document 2: a coherent, clean whitepaper-style section ------------ #
print("\n[2] Whitepaper-style section (coherent clean region)")
doc2, _ = pipeline.project(
["chronoception", "time", "awareness", "engram", "temporal"],
surface="markdown",
title="Temporal Awareness in the Engram",
subtitle="A section projected from the geometry of chronoception.",
format_spec={"genre": "whitepaper-section", "register": "technical"},
max_sections=4)
print(f" IR: {len(doc2.sections)} sections, {doc2.claim_count()} claims, "
f"ungrounded={doc2.ungrounded_count()}")
_emit_all(doc2, "engram-temporal")
# echo both markdowns so they are visible in the run log
for stem, doc in (("neuron-self", doc1), ("engram-temporal", doc2)):
print(f"\n===== GENERATED MARKDOWN — {stem} =====\n")
print(pipeline.emit(doc, "markdown").decode())
if __name__ == "__main__":
main()
-129
View File
@@ -1,129 +0,0 @@
"""geometry.py — READ-ONLY loader for a meaning-geometry region.
The efferent projector never writes to the soul. This module reaches the
geometry through the PROVEN, read-only neuron-talk faculty (``engram_client``,
GET-only, which physically refuses non-GET methods) against the running sidecar
soul. The live daemon :8742 / :7770 is contacted ONLY through that read-only
client — never mutated.
A "region" is a seed node plus a bounded neighborhood: the manifold that will
become the document's skeleton. We pool a few single-term lexical searches
(the engram search is a single-term matcher) and, when available, walk one hop
of reified neighbors, then rank by self/importance signal.
"""
from __future__ import annotations
import os
import sys
# Wire in the proven faculty (own-the-core: we reuse it, we do not fork it).
_NT = os.path.expanduser("~/Desktop/neuron-talk")
_LR = os.path.expanduser("~/Desktop/lang-realizers")
for _p in (_NT, _LR):
if _p not in sys.path:
sys.path.insert(0, _p)
from engram_client import ReadOnlyEngramClient # noqa: E402
class Region:
"""A geometry region: ranked nodes + the reified edges among them."""
def __init__(self, seed: str, nodes: list[dict], edges: list[dict]):
self.seed = seed
self.nodes = nodes # ranked engram node dicts
self.edges = edges # [{src, dst, edge, ...}]
self.by_id = {n["id"]: n for n in nodes if n.get("id")}
def __repr__(self):
return f"<Region seed={self.seed!r} nodes={len(self.nodes)} edges={len(self.edges)}>"
def _prose_quality(content: str) -> float:
"""Reward clean expository prose; penalize shouty banner-dense nodes.
A high ALLCAPS-word ratio or very short content signals a banner/telegraphic
memory node that extracts into garbage. Clean declarative prose scores high.
"""
if not content or not content.strip():
return 0.0
words = content.split()
if len(words) < 8:
return 0.1
caps = sum(1 for w in words if len(w) > 2 and w.strip(".,:;'\"-").isupper())
caps_ratio = caps / max(1, len(words))
# sentences with lowercase interior words read as prose
lower = sum(1 for w in words if w[:1].islower())
lower_ratio = lower / max(1, len(words))
return max(0.0, 1.2 * lower_ratio - 2.0 * caps_ratio)
def _relevance(content: str, terms: list[str]) -> float:
"""Topical relevance to the seed terms — keeps a region ON-THEME so a clean
but off-topic node cannot hijack the document."""
if not terms:
return 0.0
low = (content or "").lower()
hits = sum(1 for t in terms if t.lower() in low)
return hits / max(1, len(terms))
def _node_rank(n: dict, terms: list[str] | None = None) -> float:
return (float(n.get("importance") or 0.0) * 2.0
+ float(n.get("salience") or 0.0)
+ 1.5 * _prose_quality(n.get("content") or "")
+ 2.0 * _relevance(n.get("content") or "", terms or [])
+ (0.5 if (n.get("content") or "").strip() else 0.0))
def load_region(seed_terms: list[str] | str, *, client: ReadOnlyEngramClient | None = None,
max_nodes: int = 10, per_term: int = 20, hop: bool = True) -> Region:
"""Pull a bounded geometry region around ``seed_terms`` (read-only).
``seed_terms`` may be a single string or several probe terms; results are
pooled and de-duplicated. When ``hop`` and the reified neighbor endpoint is
live, one hop of neighbors is folded in so the region is a real
neighborhood, not just a keyword hit list.
"""
client = client or ReadOnlyEngramClient()
if isinstance(seed_terms, str):
seed_terms = [seed_terms]
pool: dict[str, dict] = {}
for term in seed_terms:
for n in client.search(term, limit=per_term):
if isinstance(n, dict) and n.get("id"):
pool.setdefault(n["id"], n)
ranked = sorted(pool.values(), key=lambda n: _node_rank(n, seed_terms),
reverse=True)
nodes = ranked[:max_nodes]
edges: list[dict] = []
if hop and nodes:
present = {n["id"] for n in nodes}
for n in list(nodes):
try:
for nb in client.neighbors(n["id"]):
node = nb.get("node") if isinstance(nb, dict) else None
edge = nb.get("edge") if isinstance(nb, dict) else None
if node and node.get("id"):
edges.append({"src": n["id"], "dst": node["id"],
"edge": edge})
# fold a strong neighbor into the region (bounded)
if (node["id"] not in present and len(nodes) < max_nodes + 6
and _node_rank(node, seed_terms) > 0.4):
present.add(node["id"])
nodes.append(node)
except Exception: # noqa: BLE001 — read-only best-effort; never fatal
continue
return Region(seed=", ".join(seed_terms), nodes=nodes, edges=edges)
def load_self_region(client: ReadOnlyEngramClient | None = None,
max_nodes: int = 10) -> Region:
"""The self/identity region — Neuron's own geometry, for self-description."""
return load_region(["self", "identity", "Neuron", "values", "memory",
"imprint", "consciousness"],
client=client, max_nodes=max_nodes)
-67
View File
@@ -1,67 +0,0 @@
"""pipeline.py — the Efferent Multimodal Projector, top level.
geometry region + surface/format spec
-> PLAN (manifold -> document skeleton/DAG)
-> REALIZE (proven realizer, sentence -> passage, each section faithful)
-> COHERE (document-level flow / transitions, not stitched sentences)
-> EMIT (pluggable SurfaceProjector -> the target surface)
THE SURFACE IS A PARAMETER. ``project(...)`` builds the geometry-carrying
DocumentIR once, then hands it to whichever surface projector the caller named.
Markdown, docx, and midi (music) are all the SAME IR emitted differently. That
is the efferent multimodal projector: geometry -> any surface.
"""
from __future__ import annotations
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
sys.path.insert(0, os.path.join(_HERE, "projectors"))
from cohere import cohere_document # noqa: E402
from document_ir import DocumentIR # noqa: E402
from geometry import Region, load_region # noqa: E402
from plan import plan_document # noqa: E402
from realize import realize_document # noqa: E402
# registering the projectors (import for side-effect: each self-registers)
import projectors.markdown # noqa: E402,F401
import projectors.docx # noqa: E402,F401
import projectors.midi # noqa: E402,F401
import projectors.seams # noqa: E402,F401
from projectors.base import available_surfaces, get_projector # noqa: E402
def build_ir(seed_terms, *, title: str, subtitle: str = "",
format_spec: dict | None = None,
region: Region | None = None,
max_sections: int = 8, conf_floor: float = 0.55) -> DocumentIR:
"""geometry -> PLAN -> REALIZE -> COHERE = the surface-neutral DocumentIR."""
region = region or load_region(seed_terms)
doc = plan_document(region, title=title, subtitle=subtitle,
format_spec=format_spec or {},
conf_floor=conf_floor, max_sections=max_sections)
doc = realize_document(doc)
doc = cohere_document(doc)
return doc
def emit(doc: DocumentIR, surface: str) -> bytes:
"""EMIT: project the built IR onto one surface (surface = a parameter)."""
return get_projector(surface).project(doc)
def project(seed_terms, *, surface: str, title: str, subtitle: str = "",
format_spec: dict | None = None, region: Region | None = None,
max_sections: int = 8) -> tuple[DocumentIR, bytes]:
"""The full efferent projection: geometry + surface -> (IR, bytes)."""
doc = build_ir(seed_terms, title=title, subtitle=subtitle,
format_spec=format_spec, region=region,
max_sections=max_sections)
return doc, emit(doc, surface)
__all__ = ["build_ir", "emit", "project", "available_surfaces",
"get_projector", "load_region", "DocumentIR"]
-192
View File
@@ -1,192 +0,0 @@
"""plan.py — PLAN stage: geometry region -> document skeleton (a DAG/outline).
The manifold becomes the skeleton. We extract faithful propositions from the
region's nodes (the proven neuron-talk extractor, SACRED polarity preserved),
apply a quality floor, then GROUP them into sections. Grouping is by source
node — each engram node is one coherent topic, so one salient node becomes one
section. The section ORDER is the node ranking (importance/salience): the
geometry decides the outline, not a template.
Output: a DocumentIR whose sections carry seed node ids and empty blocks. REALIZE
fills the blocks; the plan owns the structure.
"""
from __future__ import annotations
import os
import re
import sys
_NT = os.path.expanduser("~/Desktop/neuron-talk")
_LR = os.path.expanduser("~/Desktop/lang-realizers")
for _p in (_NT, _LR):
if _p not in sys.path:
sys.path.insert(0, _p)
import propositions # noqa: E402 (the proven, faithful extractor)
from document_ir import DocumentIR, Section # noqa: E402
from geometry import Region # noqa: E402
# --------------------------------------------------------------------------- #
# Proposition quality — keep only clean, well-grounded claims.
# --------------------------------------------------------------------------- #
_JUNK_RE = re.compile(r"[.][a-z]{1,3}\b|[^A-Za-z0-9 '\-]") # ".o", stray symbols
def _has_banner_token(s: str) -> bool:
"""True if any word is an ALLCAPS banner token (DHARMA, ENGRAM, MEASURED)."""
for w in (s or "").split():
core = w.strip(".,:;'\"-")
if len(core) > 2 and core.isupper():
return True
return False
def _clean_prop(p, floor: float) -> bool:
if p.confidence < floor:
return False
if not p.subject or not (p.object or (p.obj_np is not None)):
return False
subj = (p.subject or "").strip()
obj = (p.object or "").strip()
if len(subj) < 2:
return False
# banner-derived shouty fragments read as garbage in prose
if _has_banner_token(subj) or _has_banner_token(obj):
return False
if propositions._is_shouty(p.sentence or ""):
return False
# junk tokens: file-extension fragments (".o"), stray non-word symbols
if _JUNK_RE.search(subj) or _JUNK_RE.search(obj):
return False
# a proposition whose object repeats the subject is usually a parse artifact
if obj and subj.lower() == obj.lower():
return False
# a bare copula with no real complement ("X is it") reads as noise
if p.predicate == "be" and obj.lower() in ("it", "no", "nothing", "empty", ""):
return False
return True
def _dedup(props):
"""Drop duplicate claims. Two axes: (a) identical (pred,obj,polarity), and
(b) same (subject,predicate) — which collapses a mis-split compound like
"detection is post-hoc eval" -> "Detection is post/hoc/eval" into one claim
(keep the highest-confidence surface)."""
props = sorted(props, key=lambda p: p.confidence, reverse=True)
seen_po, seen_sp, out = set(), set(), []
for p in props:
subj = (p.subject or "").lower()
po = (p.predicate, (p.object or "").lower(), p.polarity)
sp = (subj, p.predicate, p.polarity)
if po in seen_po or sp in seen_sp:
continue
seen_po.add(po)
seen_sp.add(sp)
out.append(p)
return out
# --------------------------------------------------------------------------- #
# Heading derivation — a clean human heading from a node.
# --------------------------------------------------------------------------- #
_HEADING_RE = re.compile(r"^\s*#{1,4}\s+(.{2,70})\s*$", re.M)
# node-type / system labels that are NOT topical headings
_NONTOPIC_LABEL = re.compile(r"^(memory|node|knowledge|doc|session)[:/]", re.I)
def _titlecase_banner(s: str) -> str:
"""A shouty banner ("CHRONOCEPTION — SCALE-INVARIANCE") makes a fine title
once Title-cased. Keep short acronyms uppercase."""
def fix(w):
core = w.strip("—-:,.")
if len(core) <= 3 and core.isupper():
return w # acronym
return w.capitalize()
return " ".join(fix(w) for w in s.split())
def _clean_heading(text: str) -> str | None:
"""First line only, no markdown, capped, banner Title-cased. None if unusable."""
if not text:
return None
line = text.strip().splitlines()[0]
line = re.sub(r"^#+\s*", "", line).strip().strip("#").strip()
# cut at a natural break so a long banner heading stays a heading, not a para
for sep in ("", " ", ": ", ". "):
if sep in line and len(line) > 48:
line = line.split(sep)[0].strip()
break
if not (3 <= len(line) <= 64):
return None
if propositions._is_shouty(line):
line = _titlecase_banner(line)
return line or None
def _heading_for(node: dict, fallback: str) -> str:
label = (node.get("label") or "").strip()
content = node.get("content") or ""
candidates: list[str] = []
# a node-type label ("memory:remembered") is never a topic — skip it
if label and not _NONTOPIC_LABEL.match(label):
candidates.append(label)
m = _HEADING_RE.search(content)
if m:
candidates.append(m.group(1))
# the leading banner/first sentence of the content is often the real title
first = re.split(r"(?<=[.\n])", content.strip(), maxsplit=1)[0] if content.strip() else ""
candidates.append(first)
for c in candidates:
h = _clean_heading(c)
if h:
return h
return fallback
def plan_document(region: Region, *, title: str, subtitle: str = "",
format_spec: dict | None = None,
conf_floor: float = 0.55,
max_sections: int = 8,
max_claims_per_section: int = 6) -> DocumentIR:
"""Region -> DocumentIR skeleton. The geometry dictates the outline."""
format_spec = format_spec or {}
doc = DocumentIR(title=title, subtitle=subtitle,
seed_id=region.nodes[0]["id"] if region.nodes else None,
format_spec=format_spec)
made = 0
seen_headings: set[str] = set()
for node in region.nodes:
if made >= max_sections:
break
props = propositions.extract(node.get("content") or "",
node_id=node.get("id"),
node_importance=float(node.get("importance") or 0.0),
max_sentences=10)
props = [p for p in props if _clean_prop(p, conf_floor)]
props = _dedup(props)
props.sort(key=lambda p: p.confidence, reverse=True)
props = props[:max_claims_per_section]
if not props:
continue
heading = _heading_for(node, fallback=f"Region {made + 1}")
# cross-section dedup: a topic appears once. Distinguish by top claim
# subject, else drop the collision so the outline stays clean.
if heading.lower() in seen_headings:
subj = (props[0].subject or "").strip().title()
alt = f"{heading}: {subj}" if subj and subj.lower() not in heading.lower() else None
if alt and alt.lower() not in seen_headings and len(alt) <= 64:
heading = alt
else:
continue
seen_headings.add(heading.lower())
sec = Section(heading=heading, level=2, seed_ids=[node["id"]])
# stash the planned propositions on the section for REALIZE
sec.__dict__["_planned_props"] = props
sec.__dict__["_node"] = node
doc.sections.append(sec)
made += 1
return doc
-106
View File
@@ -1,106 +0,0 @@
"""base.py — the SurfaceProjector interface + registry.
THE key abstraction of the efferent projector: a projector is a pure function
from the surface-neutral, geometry-carrying DocumentIR to bytes on a target
SURFACE. The surface is a PARAMETER. Adding a surface = registering one more
projector; nothing upstream (plan/realize/cohere) changes.
DocumentIR --project--> bytes (per surface)
A TEXT projector reads ``block.sentences``. A NON-TEXT projector (music, image,
video) reads ``block.provenance`` — the geometry the IR carries — and decodes it
onto its surface. Both consume the SAME IR. That symmetry is the whole design:
the realizer generalizes into a multimodal projector, geometry -> any surface.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR # noqa: E402
@runtime_checkable
class SurfaceProjector(Protocol):
"""Geometry-document -> one surface. Implementations MUST be pure & faithful.
THE ONE SHARED SEAM. Every surface — text, music, image, video — conforms to
this single contract:
project(frame: DocumentIR) -> bytes
where ``frame`` is the geometry-carrying meaning-geometry (the SemFrame at
document scale; a single utterance is the degenerate one-section frame).
RECOMMENDED INTERNAL SHAPE (the peer music/text decomposition, blessed here
so all surfaces share it): a projector may split ``project`` into
spec = self.plan(frame) # meaning-geometry -> surface-specific spec
bytes = self.realize(spec) # spec -> surface, via this projector's PROFILE
``project`` is then ``realize(plan(frame))``. The PROFILE (a text lang-profile,
a music instr/mode-profile, an image layout-profile) is a property of the
projector instance — the pluggable knob. See :class:`TwoStageProjector`.
A TEXT projector's plan reads ``frame`` sentences; a MUSIC/IMAGE projector's
plan reads ``frame.all_provenance()`` — the geometry — and derives its spec
(pitch/harmony/rhythm, or layout) FROM the meaning, deterministically. Same
frame, different profile.
"""
surface: str # "markdown" | "docx" | "midi" | "audio" | "image" | "video"
media_type: str # MIME type of the emitted bytes
ext: str # file extension (no dot)
modality: str # "text" | "audio" | "image" | "video"
profile: object # the pluggable per-surface profile (may be None)
def project(self, doc: DocumentIR) -> bytes:
"""Emit the document on this surface. Returns raw bytes."""
...
class TwoStageProjector:
"""Optional base for the peer plan()/realize() decomposition.
Subclasses implement ``plan(frame) -> spec`` and ``realize(spec) -> bytes``;
``project`` is their composition. This is exactly the peer music interface
(spec = plan(frame, profile); surface = realize(spec, profile)) expressed so
that it still satisfies the single ``SurfaceProjector.project`` seam. Text,
music, and image projectors can all subclass this and remain interchangeable.
"""
surface: str = ""
media_type: str = ""
ext: str = ""
modality: str = ""
profile: object = None
def plan(self, doc: DocumentIR): # -> spec
raise NotImplementedError
def realize(self, spec) -> bytes:
raise NotImplementedError
def project(self, doc: DocumentIR) -> bytes:
return self.realize(self.plan(doc))
_REGISTRY: dict[str, SurfaceProjector] = {}
def register(projector: SurfaceProjector) -> SurfaceProjector:
_REGISTRY[projector.surface] = projector
return projector
def get_projector(surface: str) -> SurfaceProjector:
if surface not in _REGISTRY:
raise KeyError(f"no projector registered for surface {surface!r}; "
f"have {sorted(_REGISTRY)}")
return _REGISTRY[surface]
def available_surfaces() -> list[str]:
return sorted(_REGISTRY)
-113
View File
@@ -1,113 +0,0 @@
"""docx.py — the .docx surface projector: an OWN minimal OOXML emitter.
Own-the-core: a .docx is just a ZIP of a few XML parts (WordprocessingML). We
emit it with the standard library only — ``zipfile`` + string XML — no
python-docx, no external dependency. This proves a "richer structured format"
surface without importing anyone else's toolkit.
Parts emitted (the minimal valid set + a styles part for real headings):
[Content_Types].xml
_rels/.rels
word/_rels/document.xml.rels
word/styles.xml (Title / Heading1 / Heading2 / Normal)
word/document.xml (the content)
Like the markdown projector it reads only the IR's realized sentences; it
invents nothing. The surface differs, the faithful content does not.
"""
from __future__ import annotations
import io
import os
import sys
import zipfile
from xml.sax.saxutils import escape
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR # noqa: E402
from projectors.base import register # noqa: E402
_CONTENT_TYPES = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
</Types>"""
_RELS = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"""
_DOC_RELS = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>"""
_W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
_STYLES = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="{_W}">
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/>
<w:rPr><w:sz w:val="22"/></w:rPr></w:style>
<w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/>
<w:pPr><w:spacing w:after="240"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="52"/></w:rPr></w:style>
<w:style w:type="paragraph" w:styleId="Subtitle"><w:name w:val="Subtitle"/>
<w:rPr><w:i/><w:sz w:val="28"/><w:color w:val="555555"/></w:rPr></w:style>
<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/>
<w:pPr><w:spacing w:before="240" w:after="120"/><w:outlineLvl w:val="0"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="34"/></w:rPr></w:style>
<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/>
<w:pPr><w:spacing w:before="200" w:after="100"/><w:outlineLvl w:val="1"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="28"/></w:rPr></w:style>
</w:styles>"""
def _para(text: str, style: str | None = None) -> str:
ppr = f"<w:pPr><w:pStyle w:val=\"{style}\"/></w:pPr>" if style else ""
return (f"<w:p>{ppr}<w:r><w:t xml:space=\"preserve\">"
f"{escape(text)}</w:t></w:r></w:p>")
class DocxProjector:
surface = "docx"
media_type = ("application/vnd.openxmlformats-officedocument."
"wordprocessingml.document")
ext = "docx"
modality = "text"
def _document_xml(self, doc: DocumentIR) -> str:
body: list[str] = [_para(doc.title, "Title")]
if doc.subtitle:
body.append(_para(doc.subtitle, "Subtitle"))
abstract = doc.meta.get("abstract")
if abstract is not None and abstract.sentences:
body.append(_para(abstract.text()))
for sec in doc.sections:
style = "Heading1" if sec.level <= 1 else "Heading2"
body.append(_para(sec.heading, style))
for block in sec.blocks:
t = block.text()
if t:
body.append(_para(t))
return (f"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
f"<w:document xmlns:w=\"{_W}\"><w:body>"
+ "".join(body)
+ "<w:sectPr><w:pgSz w:w=\"12240\" w:h=\"15840\"/>"
"<w:pgMar w:top=\"1440\" w:right=\"1440\" w:bottom=\"1440\" "
"w:left=\"1440\"/></w:sectPr></w:body></w:document>")
def project(self, doc: DocumentIR) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("[Content_Types].xml", _CONTENT_TYPES)
z.writestr("_rels/.rels", _RELS)
z.writestr("word/_rels/document.xml.rels", _DOC_RELS)
z.writestr("word/styles.xml", _STYLES)
z.writestr("word/document.xml", self._document_xml(doc))
return buf.getvalue()
register(DocxProjector())
-45
View File
@@ -1,45 +0,0 @@
"""markdown.py — the Markdown surface projector (text facet).
The most tractable surface, and the reference implementation: reads the IR's
realized sentences and lays them out as Markdown. Introduces no content — it is
pure typography over the faithful text the realizer produced.
"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR # noqa: E402
from projectors.base import register # noqa: E402
class MarkdownProjector:
surface = "markdown"
media_type = "text/markdown"
ext = "md"
modality = "text"
def render_str(self, doc: DocumentIR) -> str:
lines: list[str] = [f"# {doc.title}"]
if doc.subtitle:
lines.append(f"\n*{doc.subtitle}*")
abstract = doc.meta.get("abstract")
if abstract is not None and abstract.sentences:
lines.append("")
lines.append(abstract.text())
for sec in doc.sections:
lines.append("")
lines.append(f"{'#' * max(2, sec.level)} {sec.heading}")
for block in sec.blocks:
body = block.text()
if body:
lines.append("")
lines.append(body)
return "\n".join(lines) + "\n"
def project(self, doc: DocumentIR) -> bytes:
return self.render_str(doc).encode("utf-8")
register(MarkdownProjector())
-133
View File
@@ -1,133 +0,0 @@
"""midi.py — the MUSIC surface projector: geometry -> symbolic music (MIDI).
The first NON-TEXT surface, and the proof of the general shape. "Music is
language and it is math" (Will): symbolic music is tractable and geometry-native,
so it is the natural efferent twin to try first after text.
CRUCIALLY this projector does NOT read the realized sentences. It reads the IR's
GEOMETRY facet — ``block.provenance`` — and DECODES each edge onto a musical
surface. That is the whole thesis of the multimodal projector: the same
geometry-carrying IR drives text AND music; a text projector reads the words, a
music projector reads the meaning-geometry. The mapping is deterministic and
faithful to the geometry's structure:
relation lemma -> scale degree (same relation -> same pitch class;
meaning has a consistent sonic form)
polarity -> mode (aff = major third above; neg = minor
third / lowered — SACRED polarity is
audible, a negated edge sounds negated)
confidence -> note duration (stronger grounding rings longer)
importance -> velocity (more important source = louder)
section -> phrase + register shift (structure becomes musical form)
Own-the-core: a Standard MIDI File is a header chunk + a track chunk of
delta-timed events. We emit the raw bytes with ``struct`` — no external MIDI
library. Format 0, one track.
"""
from __future__ import annotations
import io
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR, Provenance # noqa: E402
from projectors.base import TwoStageProjector, register # noqa: E402
_TICKS = 480 # ticks per quarter note
_C_MAJOR = [0, 2, 4, 5, 7, 9, 11] # semitone offsets of a diatonic scale
def _vlq(n: int) -> bytes:
"""MIDI variable-length quantity encoding of a delta time."""
if n == 0:
return b"\x00"
out = bytearray()
out.append(n & 0x7F)
n >>= 7
while n:
out.insert(0, (n & 0x7F) | 0x80)
n >>= 7
return bytes(out)
def _degree_for(relation: str) -> int:
"""Stable scale degree for a relation lemma (same relation -> same pitch)."""
if not relation:
return 0
return sum(ord(c) for c in relation.lower()) % len(_C_MAJOR)
def _note_for(p: Provenance, base: int) -> tuple[int, int, int]:
"""(pitch, velocity, duration_ticks) for one geometry edge."""
root = base + _C_MAJOR[_degree_for(p.relation)]
# polarity -> mode: affirmed edges take the bright major third, negated edges
# take the darker minor third. The negation is AUDIBLE and never dropped.
third = 4 if p.polarity == "aff" else 3
pitch = max(24, min(96, root + (third if p.confidence >= 0.5 else 0)))
velocity = int(56 + 60 * min(1.0, max(0.0, p.importance)))
velocity = max(40, min(120, velocity))
# confidence -> duration: quarter .. dotted-half
dur = int(_TICKS * (0.5 + 1.5 * min(1.0, max(0.0, p.confidence))))
return pitch, velocity, dur
# a mode-profile: the pluggable musical knob (the peer's mode_profile). Scale +
# tempo. Swapping this profile re-voices the SAME geometry — surface as parameter.
_DEFAULT_PROFILE = {"scale": _C_MAJOR, "tempo_us": 500000,
"registers": [60, 55, 64, 50, 67, 48], "program": 0}
class MidiProjector(TwoStageProjector):
"""geometry -> symbolic music, in the shared two-stage shape.
``plan(frame)`` -> a music_spec: an ordered list of note dicts derived
deterministically from the frame's provenance geometry
(the peer's ``plan(frame, profile) -> spec``).
``realize(spec)`` -> Standard MIDI File bytes (the peer's
``realize(spec, profile) -> surface``; here the surface
is symbolic MIDI, the minimal audio proof — a richer
additive-synth audio projector conforms identically).
"""
surface = "midi"
media_type = "audio/midi"
ext = "mid"
modality = "audio"
def __init__(self, profile: dict | None = None):
self.profile = profile or _DEFAULT_PROFILE
# -- stage 1: meaning-geometry -> music_spec (reads the GEOMETRY facet) -- #
def plan(self, doc: DocumentIR) -> list[dict]:
registers = self.profile["registers"]
spec: list[dict] = []
for si, sec in enumerate(doc.sections):
base = registers[si % len(registers)]
provs = [p for p in sec.all_provenance()
if p.kind in ("fact", "interpretation")]
for i, p in enumerate(provs):
pitch, vel, dur = _note_for(p, base)
spec.append({"pitch": pitch, "velocity": vel, "dur": dur,
"rest_before": (_TICKS // 2) if (si > 0 and i == 0) else 0,
"relation": p.relation, "polarity": p.polarity})
return spec
# -- stage 2: music_spec -> MIDI bytes (own-core, no library) ------------ #
def realize(self, spec: list[dict]) -> bytes:
ev = bytearray()
ev += _vlq(0) + b"\xFF\x51\x03" + struct.pack(">I", self.profile["tempo_us"])[1:]
ev += _vlq(0) + bytes([0xC0, self.profile["program"] & 0x7F])
for note in spec:
ev += _vlq(note["rest_before"]) + bytes([0x90, note["pitch"], note["velocity"]])
ev += _vlq(note["dur"]) + bytes([0x80, note["pitch"], 0])
ev += _vlq(0) + b"\xFF\x2F\x00"
track = bytes(ev)
buf = io.BytesIO()
buf.write(b"MThd" + struct.pack(">IHHH", 6, 0, 1, _TICKS))
buf.write(b"MTrk" + struct.pack(">I", len(track)) + track)
return buf.getvalue()
register(MidiProjector())
-60
View File
@@ -1,60 +0,0 @@
"""seams.py — documented efferent seams for IMAGE and VIDEO surfaces.
These are NOT implemented (per the build rails: architect, do not overbuild).
They are registered as first-class seams so the interface PROVES it accepts
future non-text projectors without any upstream change. Each documents exactly
what its decoder would read from the geometry-carrying IR, making the multimodal
generalization concrete rather than hand-wavy.
The symmetry that guarantees these are possible, not moonshots: they are the
efferent twins of multimodal INGEST. If meaning can HOLD an image (ingest as
first-class geometry), meaning can PROJECT one back. Video = image x sound x
TIME, and the engram already stores time (chronoception). So video falls out of
an image projector + the music projector + the stored temporal ordering.
"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from document_ir import DocumentIR # noqa: E402
from projectors.base import register # noqa: E402
class _Seam:
"""A registered-but-unimplemented projector. Names its decoder contract."""
def project(self, doc: DocumentIR) -> bytes: # pragma: no cover - seam
raise NotImplementedError(
f"{self.surface!r} projector is a documented seam, not yet built. "
f"Decoder contract: {self.decoder_contract}")
class ImageProjector(_Seam):
surface = "image"
media_type = "image/png"
ext = "png"
modality = "image"
decoder_contract = (
"reads block.provenance as a spatial layout — nodes become regions, edges "
"become adjacencies; salience/importance drive size/contrast; polarity "
"drives figure/ground. The efferent twin of image ingest (a geometry->raster "
"decoder, learned or engineered), exactly mirroring the embedder that turned "
"the image INTO geometry.")
class VideoProjector(_Seam):
surface = "video"
media_type = "video/mp4"
ext = "mp4"
modality = "video"
decoder_contract = (
"image x sound x TIME. Composes the image projector (per-keyframe geometry "
"layout) with the midi/music projector (score) along the geometry's stored "
"temporal ordering (chronoception). Needs no new principle once image + music "
"exist — only a muxer.")
register(ImageProjector())
register(VideoProjector())
-63
View File
@@ -1,63 +0,0 @@
"""provenance.py — the faithfulness audit + geometry->section trace.
A document projected from geometry is only worth anything if every claim traces
back. This module walks the DocumentIR and proves the discipline held:
* ZERO ungrounded claims (every fact/interpretation has a real node id),
* every emitted sentence maps to a geometry edge (or is a marked connective),
* SACRED polarity survived (negations are reported, never silently dropped),
* COHERE introduced no new geometry (connectives carry no claim).
It emits both a machine verdict and a human-readable geometry->section table.
"""
from __future__ import annotations
from document_ir import DocumentIR
def audit(doc: DocumentIR) -> dict:
provs = doc.all_provenance()
facts = [p for p in provs if p.kind in ("fact", "interpretation")]
connectives = [p for p in provs if p.kind == "connective"]
ungrounded = [p for p in facts if not p.node_id]
negations = [p for p in facts if p.polarity == "neg"]
node_ids = sorted({p.node_id for p in facts if p.node_id})
return {
"claims": len(facts),
"connectives": len(connectives),
"ungrounded_claims": len(ungrounded),
"negations_preserved": len(negations),
"distinct_source_nodes": len(node_ids),
"faithful": len(ungrounded) == 0,
"source_nodes": node_ids,
}
def trace_table(doc: DocumentIR) -> str:
"""Human-readable geometry -> section -> claim provenance table."""
lines = ["# Provenance — every claim traces geometry", ""]
lines.append(f"**Document:** {doc.title}")
a = audit(doc)
lines.append(f"**Claims:** {a['claims']} · **Ungrounded:** "
f"{a['ungrounded_claims']} · **Negations preserved:** "
f"{a['negations_preserved']} · **Source nodes:** "
f"{a['distinct_source_nodes']} · **Faithful:** "
f"{'YES' if a['faithful'] else 'NO'}")
lines.append("")
for si, sec in enumerate(doc.sections, 1):
lines.append(f"## {si}. {sec.heading}")
lines.append(f"_seed nodes: {', '.join(i[:8] for i in sec.seed_ids)}_")
lines.append("")
lines.append("| # | realized claim | traces geometry edge |")
lines.append("|---|----------------|----------------------|")
n = 0
for block in sec.blocks:
for sent, prov in zip(block.sentences, block.provenance):
if prov.kind == "connective":
continue
n += 1
edge = prov.trace().replace("|", "\\|")
s = sent.replace("|", "\\|")
lines.append(f"| {n} | {s} | {edge} |")
lines.append("")
return "\n".join(lines) + "\n"
-112
View File
@@ -1,112 +0,0 @@
"""realize.py — REALIZE stage: fill each planned section with faithful passages.
Scales the PROVEN realizer from a single assertion to a passage. For each
planned proposition we build a realizer-ready clause (the proven
``_prop_to_clause`` mapping) and run it through the proven engine
(``engine.realize``), which is a deterministic grammar with the SACRED negation
contract — it never invents. Each realized sentence is paired with a
:class:`Provenance` that pins it to the exact geometry edge it came from.
"Passage, not a list of sentences": within a section we lightly vary sentence
openings and group related claims, but we add NO content the geometry did not
assert. The only non-geometry words are function words the grammar already owns
(articles, "and", conjunction of same-subject claims). Document-level flow is
COHERE's job; this stage owns intra-section fluency + fidelity.
"""
from __future__ import annotations
import os
import sys
_NT = os.path.expanduser("~/Desktop/neuron-talk")
_LR = os.path.expanduser("~/Desktop/lang-realizers")
for _p in (_NT, _LR):
if _p not in sys.path:
sys.path.insert(0, _p)
import engine # noqa: E402 (the proven no-LLM realizer)
from dialogue import _prop_to_clause # noqa: E402 (proven prop -> clause)
from document_ir import Block, DocumentIR, Provenance, Section # noqa: E402
def _provenance_from(p, kind: str = "fact") -> Provenance:
return Provenance(
subj_id=p.source_node_id, subject=p.subject, relation=p.predicate,
obj=p.object, polarity=p.polarity, confidence=round(float(p.confidence), 3),
node_id=p.source_node_id, kind=kind,
importance=float(getattr(p, "node_importance", 0.0) or 0.0),
salience=0.0,
)
import re as _re
# a well-formed declarative opens with a determiner, a proper noun, "I", or a
# capitalized head — not a mis-parsed object pronoun or a copula fragment.
_BAD_OPENERS = _re.compile(r"^(Me |It is I|There is|This is it|That is it)\b")
_VACUOUS = _re.compile(r"^\w+ (is|are|was|were) (it|no|nothing|empty|those|this|that)\.?$",
_re.I)
def _good_sentence(text: str) -> bool:
"""Fluency gate — drops degenerate realizations. NEVER loosens faithfulness;
it only refuses to SPEAK a claim whose surface came out malformed."""
words = text.rstrip(".").split()
if len(words) < 3:
return False
if _BAD_OPENERS.search(text):
return False
if _VACUOUS.match(text):
return False
# a sentence that is mostly one-letter/two-letter tokens is a parse artifact
short = sum(1 for w in words if len(w.strip(".,'")) <= 2)
if short > len(words) / 2:
return False
return True
def _realize_prop(p, lang: str = "en") -> tuple[str, Provenance] | None:
"""One proposition -> (faithful sentence, provenance) or None if it drops."""
clause = _prop_to_clause(p)
text = engine.realize(clause, lang)
if not text or not text.strip():
return None
text = text.strip()
if not text.endswith((".", "!", "?")):
text += "."
# capitalize first character (proper nouns / "I" already handled by grammar)
text = text[0].upper() + text[1:]
if not _good_sentence(text):
return None
return text, _provenance_from(p)
def realize_document(doc: DocumentIR, lang: str = "en") -> DocumentIR:
"""Fill every planned section's blocks with faithful, realized passages."""
for sec in doc.sections:
planned = sec.__dict__.get("_planned_props", [])
block = Block(role="body")
summary_bits: list[str] = []
for p in planned:
r = _realize_prop(p, lang)
if r is None:
continue
text, prov = r
block.sentences.append(text)
block.provenance.append(prov)
if len(summary_bits) < 1:
# a short grounded gloss for TOC / pptx bullets
obj = (prov.obj or "").strip().rstrip(".")
if obj:
summary_bits.append(obj)
if block.sentences:
sec.blocks.append(block)
sec.summary = summary_bits[0] if summary_bits else ""
# drop the transient planning payload; the IR is now self-contained
sec.__dict__.pop("_planned_props", None)
sec.__dict__.pop("_node", None)
# prune sections that realized to nothing
doc.sections = [s for s in doc.sections if s.blocks]
return doc
-73
View File
@@ -1,73 +0,0 @@
// audio-demo.el - Drive the native audio surface: render a tone per instrument
// from its LEARNED signature, then render a small meaning-phrase "piece".
// Entry point: top-level statement calls main() (same convention as the
// examples' top-level println(run_test())).
fn micros_to_str(xs: [Int]) -> String {
let n: Int = native_list_len(xs)
let out: String = ""
let i: Int = 0
while i < n {
if i > 0 { let out: String = out + "," }
let out: String = out + int_to_str(native_list_get(xs, i))
let i: Int = i + 1
}
return out
}
// Render a 1.0s A4 (midi 69) tone from a signature file, print the parsed
// partials (proving the numbers came from the engram .sig), write the WAV.
fn render_tone(name: String, sigpath: String, outpath: String, table: [Int]) -> Int {
let lines: [String] = sig_load(sigpath)
let partials: [Int] = parse_micros(sig_field(lines, "partials"))
println("[" + name + "] partials_n=" + sig_field(lines, "partials_n") + " parsed_partials_micro(scale 1e6)=" + micros_to_str(partials))
println("[" + name + "] raw partials line from .sig = " + sig_field(lines, "partials"))
let freq: Int = freq_of_midi(69)
let note: [Int] = synth_from_sig(lines, freq, 1000, 900, 44100, table)
let n: Int = native_list_len(note)
let ok: Int = wav_write(outpath, note, n, 44100)
println("[" + name + "] rendered " + int_to_str(n) + " samples -> " + outpath + " (write_ok=" + int_to_str(ok) + ")")
return n
}
fn run_demo() -> Int {
let table: [Int] = sin_table()
fs_mkdir("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out")
println("=== TONES: render A4 (midi 69) from each learned signature ===")
render_tone("flute", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/flute.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-flute.wav", table)
render_tone("clarinet", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/clarinet.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-clarinet.wav", table)
render_tone("violin", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/violin.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-violin.wav", table)
render_tone("piano", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/piano.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-piano.wav", table)
render_tone("organ", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/organ.sig", "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/tone-organ.wav", table)
println("")
println("=== PIECE: a 6-frame meaning phrase (incl. a NEG frame) ===")
let frames: [[String]] = native_list_empty()
let frames: [[String]] = native_list_append(frames, audio_frame("agent", "aff", "0.9", "0.8", "0", "s1"))
let frames: [[String]] = native_list_append(frames, audio_frame("theme", "aff", "0.7", "0.6", "0", "s2"))
let frames: [[String]] = native_list_append(frames, audio_frame("cause", "aff", "0.8", "0.9", "1", "s3"))
let frames: [[String]] = native_list_append(frames, audio_frame("negation", "neg", "0.85", "0.7", "0", "s4"))
let frames: [[String]] = native_list_append(frames, audio_frame("goal", "aff", "0.6", "0.5", "1", "s5"))
let frames: [[String]] = native_list_append(frames, audio_frame("result", "aff", "0.95", "1.0", "0", "s6"))
// Print the plan so the NEG frame's minor third (+3) vs major (+4) is visible.
let nf: Int = native_list_len(frames)
let fi: Int = 0
while fi < nf {
let frame: [String] = native_list_get(frames, fi)
let plan: [Int] = plan_note(frame)
let pol: String = surface_get(frame, "polarity")
let third_name: String = "major(+4)"
if str_eq(pol, "neg") { let third_name: String = "MINOR(+3)" }
println("frame " + int_to_str(fi) + " relation=" + surface_get(frame, "relation") + " polarity=" + pol + " -> midi=" + int_to_str(native_list_get(plan, 0)) + " dur_ms=" + int_to_str(native_list_get(plan, 1)) + " amp_pm=" + int_to_str(native_list_get(plan, 2)) + " third=" + third_name)
let fi: Int = fi + 1
}
let piano_lines: [String] = sig_load("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/piano.sig")
let total: Int = realize_audio(frames, piano_lines, "/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/piece.wav", 44100, table)
println("PIECE rendered " + int_to_str(total) + " samples -> /Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/piece.wav")
return total
}
println("audio-demo main returned samples=" + int_to_str(run_demo()))
-400
View File
@@ -1,400 +0,0 @@
// audio-surface.el - Native own-core additive-synthesis audio surface.
//
// The AUDIO efferent seam, native, no Python and no library. This renders real
// PCM .wav bytes from instrument SIGNATURES read from engram-sourced .sig data
// files (elp/faculty/sig/*.sig) - the partial amplitudes are NEVER literals in
// this source; they are parsed from the learned signature at run time. That is
// the whole proof: render-from-learned-signatures.
//
// EL has no float arithmetic operator (codegen emits raw int64 ops for + - * /
// on the shared 64-bit slot) and no float-arithmetic natives - so ALL synthesis
// math here is own-core INTEGER fixed-point. Angles use a quarter-wave sine
// table (scale 10000) from a fixed-point Taylor series; amplitudes are parsed to
// micro (scale 1e6) straight from the .sig text; frequencies are milliHz ints.
//
// Pipeline mirrors the two-stage projector (midi.py): plan_note(frame) reads a
// frame's meaning-geometry slot-map and derives (pitch, duration, amplitude);
// realize_audio SUPERPOSES the signature's partials (the compose op) and
// serialises RIFF/WAVE. Same frame -> midi OR audio.
// -- integer decimal + string helpers -----------------------------------------
fn str_to_int_el(s: String) -> Int {
let n: Int = str_len(s)
let i: Int = 0
let v: Int = 0
let neg: Bool = false
while i < n {
let c: Int = str_char_code(s, i)
if c == 45 { let neg: Bool = true }
if c >= 48 {
if c < 58 {
let v: Int = v * 10 + (c - 48)
}
}
let i: Int = i + 1
}
if neg { return 0 - v }
return v
}
fn parse_micro(s: String) -> Int {
let dot: Int = str_index_of(s, ".")
if dot < 0 {
return str_to_int_el(s) * 1000000
}
let n: Int = str_len(s)
let ipart: String = str_slice(s, 0, dot)
let fpart: String = str_slice(s, dot + 1, n)
let iv: Int = str_to_int_el(ipart)
let fv: Int = 0
let scale: Int = 100000
let fn2: Int = str_len(fpart)
let i: Int = 0
while i < 6 {
let d: Int = 0
if i < fn2 {
let d: Int = str_char_code(fpart, i) - 48
}
let fv: Int = fv + d * scale
let scale: Int = scale / 10
let i: Int = i + 1
}
return iv * 1000000 + fv
}
// -- signature (engram data file) loader ---------------------------------------
fn sig_load(path: String) -> [String] {
let text: String = fs_read(path)
return str_split(text, "\n")
}
fn sig_field(lines: [String], key: String) -> String {
let pref: String = key + ": "
let n: Int = native_list_len(lines)
let plen: Int = str_len(pref)
let i: Int = 0
while i < n {
let ln: String = native_list_get(lines, i)
if str_starts_with(ln, pref) {
return str_slice(ln, plen, str_len(ln))
}
let i: Int = i + 1
}
return ""
}
fn parse_micros(csv: String) -> [Int] {
let parts: [String] = str_split(csv, ",")
let n: Int = native_list_len(parts)
let out: [Int] = native_list_empty()
let i: Int = 0
while i < n {
let out: [Int] = native_list_append(out, parse_micro(native_list_get(parts, i)))
let i: Int = i + 1
}
return out
}
// -- fixed-point sine (own-core, quarter-wave Taylor table, scale 10000) --------
fn sin_table() -> [Int] {
let HP: Int = 1570796
let t: [Int] = native_list_empty()
let q: Int = 0
while q < 257 {
let x: Int = q * HP / 256
let x2: Int = x * x / 1000000
let x3: Int = x2 * x / 1000000
let x5: Int = x3 * x2 / 1000000
let x7: Int = x5 * x2 / 1000000
let x9: Int = x7 * x2 / 1000000
let s: Int = x - x3 / 6 + x5 / 120 - x7 / 5040 + x9 / 362880
let t: [Int] = native_list_append(t, s / 100)
let q: Int = q + 1
}
return t
}
fn sin_lookup(t: [Int], phase: Int) -> Int {
let p: Int = phase % 1024
if p < 0 { let p: Int = p + 1024 }
let quad: Int = p / 256
let r: Int = p % 256
if quad == 0 { return native_list_get(t, r) }
if quad == 1 { return native_list_get(t, 256 - r) }
if quad == 2 { return 0 - native_list_get(t, r) }
return 0 - native_list_get(t, 256 - r)
}
fn isqrt_int(n: Int) -> Int {
if n <= 0 { return 0 }
let x: Int = n
let y: Int = (x + 1) / 2
while y < x {
let x: Int = y
let y: Int = (x + n / x) / 2
}
return x
}
// freq_of_midi: equal-tempered frequency in milliHz. 440000 mHz at midi 69.
fn freq_of_midi(m: Int) -> Int {
let f: Int = 440000
if m > 69 {
let k: Int = m - 69
let i: Int = 0
while i < k {
let f: Int = f * 1059463 / 1000000
let i: Int = i + 1
}
return f
}
if m < 69 {
let k: Int = 69 - m
let i: Int = 0
while i < k {
let f: Int = f * 1000000 / 1059463
let i: Int = i + 1
}
return f
}
return f
}
// -- envelope (ADSR), scale 1000 -----------------------------------------------
fn adsr_env(i: Int, total: Int, atk_n: Int, dec_n: Int, sus_pm: Int, rel_n: Int) -> Int {
if i < atk_n {
if atk_n == 0 { return 1000 }
return 1000 * i / atk_n
}
if i < atk_n + dec_n {
if dec_n == 0 { return sus_pm }
return 1000 - (1000 - sus_pm) * (i - atk_n) / dec_n
}
let rel_start: Int = total - rel_n
if i < rel_start {
return sus_pm
}
if rel_n == 0 { return 0 }
let left: Int = total - i
return sus_pm * left / rel_n
}
// -- note synthesis: SUPERPOSE the learned partials -> [Int] samples -----------
fn note_samples(freq_mHz: Int, dur_ms: Int, rate: Int, partials: [Int], sumP: Int, b_micro: Int, vib_rate: Int, vib_cents: Int, atk_ms: Int, dec_ms: Int, sus_pm: Int, rel_ms: Int, amp_pm: Int, table: [Int]) -> [Int] {
let total: Int = dur_ms * rate / 1000
let atk_n: Int = atk_ms * rate / 1000
let dec_n: Int = dec_ms * rate / 1000
let rel_n: Int = rel_ms * rate / 1000
let np: Int = native_list_len(partials)
let half_mhz: Int = rate * 1000 / 2
let out: [Int] = native_list_empty()
let i: Int = 0
while i < total {
let acc: Int = 0
let k: Int = 0
while k < np {
let harm: Int = k + 1
let amp_k: Int = native_list_get(partials, k)
let factor: Int = 1000000
if b_micro > 0 {
let val: Int = 1000000 + b_micro * harm * harm
let factor: Int = isqrt_int(val * 1000000)
}
let fn_mhz: Int = freq_mHz * harm
let fn_mhz: Int = fn_mhz * factor / 1000000
if vib_cents > 0 {
if vib_rate > 0 {
let vphase: Int = i * vib_rate * 1024 / rate
let vs: Int = sin_lookup(table, vphase)
let vibf: Int = 1000000 + (vib_cents * vs * 833) / 10000
let fn_mhz: Int = fn_mhz * vibf / 1000000
}
}
if fn_mhz <= half_mhz {
let phase: Int = i * fn_mhz * 1024 / (rate * 1000)
let sv: Int = sin_lookup(table, phase)
let acc: Int = acc + sv * amp_k / 1000000
}
let k: Int = k + 1
}
let env: Int = adsr_env(i, total, atk_n, dec_n, sus_pm, rel_n)
let s16: Int = acc * 2800000 / sumP
let s16: Int = s16 * env / 1000
let s16: Int = s16 * amp_pm / 1000
if s16 > 32767 { let s16: Int = 32767 }
if s16 < 0 - 32767 { let s16: Int = 0 - 32767 }
let out: [Int] = native_list_append(out, s16)
let i: Int = i + 1
}
return out
}
fn synth_from_sig(lines: [String], freq_mHz: Int, dur_ms: Int, amp_pm: Int, rate: Int, table: [Int]) -> [Int] {
let partials: [Int] = parse_micros(sig_field(lines, "partials"))
let np: Int = native_list_len(partials)
let sumP: Int = 0
let j: Int = 0
while j < np {
let pj: Int = native_list_get(partials, j)
let sumP: Int = sumP + pj
let j: Int = j + 1
}
if sumP <= 0 { let sumP: Int = 1000000 }
let adsr: [String] = str_split(sig_field(lines, "adsr"), ",")
let atk_ms: Int = parse_micro(native_list_get(adsr, 0)) / 1000
let dec_ms: Int = parse_micro(native_list_get(adsr, 1)) / 1000
let sus_pm: Int = parse_micro(native_list_get(adsr, 2)) / 1000
let rel_ms: Int = parse_micro(native_list_get(adsr, 3)) / 1000
let b_micro: Int = parse_micro(sig_field(lines, "inharmonicity_B"))
let vib_rate: Int = str_to_int_el(sig_field(lines, "vibrato_rate_hz"))
let vib_cents: Int = str_to_int_el(sig_field(lines, "vibrato_depth_cents"))
return note_samples(freq_mHz, dur_ms, rate, partials, sumP, b_micro, vib_rate, vib_cents, atk_ms, dec_ms, sus_pm, rel_ms, amp_pm, table)
}
// -- byte-buffer helpers (own-core, no library) --------------------------------
fn put_tag(buf: String, pos: Int, s: String) -> String {
let n: Int = str_len(s)
let i: Int = 0
while i < n {
let buf: String = __str_set_char(buf, pos + i, str_char_code(s, i))
let i: Int = i + 1
}
return buf
}
fn put_u32le(buf: String, pos: Int, v: Int) -> String {
let buf: String = __str_set_char(buf, pos, v % 256)
let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256)
let buf: String = __str_set_char(buf, pos + 2, (v / 65536) % 256)
let buf: String = __str_set_char(buf, pos + 3, (v / 16777216) % 256)
return buf
}
fn put_u16le(buf: String, pos: Int, v: Int) -> String {
let buf: String = __str_set_char(buf, pos, v % 256)
let buf: String = __str_set_char(buf, pos + 1, (v / 256) % 256)
return buf
}
// -- WAV serializer: own-core RIFF/WAVE, PCM mono 16-bit -----------------------
fn wav_write(path: String, samples: [Int], n: Int, rate: Int) -> Int {
let data_len: Int = n * 2
let total: Int = 44 + data_len
let buf: String = __str_alloc(total)
let buf: String = put_tag(buf, 0, "RIFF")
let buf: String = put_u32le(buf, 4, 36 + data_len)
let buf: String = put_tag(buf, 8, "WAVE")
let buf: String = put_tag(buf, 12, "fmt ")
let buf: String = put_u32le(buf, 16, 16)
let buf: String = put_u16le(buf, 20, 1)
let buf: String = put_u16le(buf, 22, 1)
let buf: String = put_u32le(buf, 24, rate)
let buf: String = put_u32le(buf, 28, rate * 2)
let buf: String = put_u16le(buf, 32, 2)
let buf: String = put_u16le(buf, 34, 16)
let buf: String = put_tag(buf, 36, "data")
let buf: String = put_u32le(buf, 40, data_len)
let i: Int = 0
while i < n {
let v: Int = native_list_get(samples, i)
if v < 0 { let v: Int = v + 65536 }
let buf: String = __str_set_char(buf, 44 + i * 2, v % 256)
let buf: String = __str_set_char(buf, 44 + i * 2 + 1, (v / 256) % 256)
let i: Int = i + 1
}
let ok: Int = fs_write_bytes(path, buf, total)
return ok
}
// -- plan: frame slot-map -> note atom (pitch, duration, amplitude) ------------
fn audio_frame(relation: String, polarity: String, confidence: String, importance: String, salience: String, subj_id: String) -> [String] {
let f: [String] = native_list_empty()
let f: [String] = native_list_append(f, "relation")
let f: [String] = native_list_append(f, relation)
let f: [String] = native_list_append(f, "polarity")
let f: [String] = native_list_append(f, polarity)
let f: [String] = native_list_append(f, "confidence")
let f: [String] = native_list_append(f, confidence)
let f: [String] = native_list_append(f, "importance")
let f: [String] = native_list_append(f, importance)
let f: [String] = native_list_append(f, "salience")
let f: [String] = native_list_append(f, salience)
let f: [String] = native_list_append(f, "subj_id")
let f: [String] = native_list_append(f, subj_id)
return f
}
fn degree_offset(deg: Int) -> Int {
if deg == 0 { return 0 }
if deg == 1 { return 2 }
if deg == 2 { return 4 }
if deg == 3 { return 5 }
if deg == 4 { return 7 }
if deg == 5 { return 9 }
return 11
}
// returns [midi, dur_ms, amp_pm]
fn plan_note(frame: [String]) -> [Int] {
let relation: String = surface_get(frame, "relation")
let polarity: String = surface_get(frame, "polarity")
let confidence: String = surface_get(frame, "confidence")
let importance: String = surface_get(frame, "importance")
let salience: String = surface_get(frame, "salience")
let rn: Int = str_len(relation)
let csum: Int = 0
let i: Int = 0
while i < rn {
let cc: Int = str_char_code(relation, i)
let csum: Int = csum + cc
let i: Int = i + 1
}
let deg: Int = csum % 7
let third: Int = 4
if str_eq(polarity, "neg") { let third: Int = 3 }
let sal_oct: Int = str_to_int_el(salience)
let doff: Int = degree_offset(deg)
let midi: Int = 60 + sal_oct * 12 + doff + third
let conf_micro: Int = parse_micro(confidence)
let dur_ms: Int = 200 + conf_micro / 1000
let imp_micro: Int = parse_micro(importance)
let amp_pm: Int = 400 + imp_micro / 2000
let out: [Int] = native_list_empty()
let out: [Int] = native_list_append(out, midi)
let out: [Int] = native_list_append(out, dur_ms)
let out: [Int] = native_list_append(out, amp_pm)
return out
}
fn realize_audio(frames: [[String]], sig_lines: [String], path: String, rate: Int, table: [Int]) -> Int {
let nf: Int = native_list_len(frames)
let all: [Int] = native_list_empty()
let count: Int = 0
let fi: Int = 0
while fi < nf {
let frame: [String] = native_list_get(frames, fi)
let plan: [Int] = plan_note(frame)
let midi: Int = native_list_get(plan, 0)
let dur_ms: Int = native_list_get(plan, 1)
let amp_pm: Int = native_list_get(plan, 2)
let freq: Int = freq_of_midi(midi)
let note: [Int] = synth_from_sig(sig_lines, freq, dur_ms, amp_pm, rate, table)
let nn: Int = native_list_len(note)
let j: Int = 0
while j < nn {
let all: [Int] = native_list_append(all, native_list_get(note, j))
let j: Int = j + 1
}
let count: Int = count + nn
let fi: Int = fi + 1
}
let ok: Int = wav_write(path, all, count, rate)
return count
}
-1
View File
@@ -1 +0,0 @@
import "language-profile.el"
-2
View File
@@ -1,2 +0,0 @@
import "language-profile.el"
import "dedup_test_a_nodedup.el"
-2
View File
@@ -1,2 +0,0 @@
import "language-profile.el"
extern fn fn_a(x: String) -> String
-1
View File
@@ -1 +0,0 @@
extern fn fn_a(x: String) -> String
-2
View File
@@ -1,2 +0,0 @@
import "language-profile.el"
extern fn fn_a(x: String) -> String
-6
View File
@@ -1,6 +0,0 @@
import "language-profile.el"
import "dedup_test_a.el"
fn main_fn(x: String) -> String {
return x
}
-2
View File
@@ -1,2 +0,0 @@
import "language-profile.el"
import "dedup_test_a.el"
-2
View File
@@ -1,2 +0,0 @@
import "language-profile.el"
import "dedup_test_a_notail.el"
-159
View File
@@ -1,159 +0,0 @@
// elp.el - Engram Language Protocol public API.
//
// Output half of the ELP: Engram semantic form natural language surface text.
// 31 languages. Ties together language-profile, vocabulary, morphology,
// grammar, realizer, and semantics into a single entry point.
//
// Import chain (mirrors manifest.el dependency order):
// language-profile (no deps)
// vocabulary (no deps)
// morphology (depends on: language-profile)
// morphology-XX (depends on: morphology) all language engines
// grammar (depends on: language-profile)
// realizer (depends on: morphology, grammar, language-profile)
// semantics (depends on: grammar, realizer, language-profile)
//
// When elc processes a source that imports this file, it resolves all
// transitive imports via depth-first deduplication each module is
// inlined exactly once regardless of how many importers reference it.
// Base layers
import "language-profile.el"
import "vocabulary.el"
// Morphology: base engine
import "morphology.el"
// Morphology: living languages
import "morphology-es.el"
import "morphology-fr.el"
import "morphology-de.el"
import "morphology-ru.el"
import "morphology-ja.el"
import "morphology-fi.el"
import "morphology-ar.el"
import "morphology-hi.el"
import "morphology-sw.el"
// Morphology: ancient / classical
import "morphology-la.el"
import "morphology-he.el"
// Morphology: dead languages
import "morphology-grc.el"
import "morphology-ang.el"
import "morphology-sa.el"
import "morphology-got.el"
import "morphology-non.el"
import "morphology-enm.el"
import "morphology-pi.el"
import "morphology-fro.el"
import "morphology-goh.el"
import "morphology-sga.el"
import "morphology-txb.el"
import "morphology-peo.el"
import "morphology-akk.el"
import "morphology-uga.el"
import "morphology-egy.el"
import "morphology-sux.el"
import "morphology-gez.el"
import "morphology-cop.el"
// Higher layers
import "grammar.el"
import "realizer.el"
import "semantics.el"
//
// Entry points:
//
// generate(semantic_form_json) -> String
// Low-level JSON-based API, defaults to English. SemanticForm JSON fields:
// intent - "assert" | "question" | "command"
// agent - subject (pronoun or noun phrase, optional for commands)
// predicate - verb base form
// patient - object noun phrase (optional)
// location - prepositional phrase e.g. "in the park" (optional)
// tense - "present" | "past" | "future" (default: "present")
// aspect - "simple" | "progressive" | "perfect" (default: "simple")
// lang - ISO 639-1 code (default: "en")
//
// generate_lang(semantic_form_json, lang_code) -> String
// JSON-based API with explicit language code (overrides any "lang" in JSON).
//
// generate_frame(frame: SemFrame) -> String
// High-level SemFrame API. Language from frame's "lang" field (default "en").
// Intents: "assert" | "query" | "describe" | "greet".
//
// generate_frame_lang(frame: SemFrame, lang_code: String) -> String
// High-level SemFrame API with explicit language code override.
// JSON helpers
fn sem_get(json: String, key: String) -> String {
let val: String = json_get(json, key)
return val
}
// Public API: SemFrame
// Generate text from a SemFrame in the language embedded in the frame (default "en").
fn generate_frame(frame: [String]) -> String {
return sem_realize(frame)
}
// Generate text from a SemFrame in the specified language.
fn generate_frame_lang(frame: [String], lang_code: String) -> String {
return sem_realize_lang(frame, lang_code)
}
// Public API: JSON
// Build a realizer slot map from JSON fields and an explicit lang code.
fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String] {
let intent: String = sem_get(semantic_form_json, "intent")
let agent: String = sem_get(semantic_form_json, "agent")
let predicate: String = sem_get(semantic_form_json, "predicate")
let patient: String = sem_get(semantic_form_json, "patient")
let location: String = sem_get(semantic_form_json, "location")
let tense: String = sem_get(semantic_form_json, "tense")
let aspect: String = sem_get(semantic_form_json, "aspect")
let form: [String] = native_list_empty()
let form = native_list_append(form, "intent")
let form = native_list_append(form, intent)
let form = native_list_append(form, "agent")
let form = native_list_append(form, agent)
let form = native_list_append(form, "predicate")
let form = native_list_append(form, predicate)
let form = native_list_append(form, "patient")
let form = native_list_append(form, patient)
let form = native_list_append(form, "location")
let form = native_list_append(form, location)
let form = native_list_append(form, "tense")
let form = native_list_append(form, tense)
let form = native_list_append(form, "aspect")
let form = native_list_append(form, aspect)
let form = native_list_append(form, "lang")
let form = native_list_append(form, lang_code)
return form
}
// Generate text from a JSON semantic form. Language defaults to "en" unless
// the JSON contains a "lang" field.
fn generate(semantic_form_json: String) -> String {
let lang_in_json: String = sem_get(semantic_form_json, "lang")
let lang_code: String = lang_in_json
if str_eq(lang_code, "") {
let lang_code = "en"
}
let form: [String] = build_form_from_json(semantic_form_json, lang_code)
return realize(form)
}
// Generate text from a JSON semantic form in the specified language.
// lang_code overrides any "lang" field present in the JSON.
fn generate_lang(semantic_form_json: String, lang_code: String) -> String {
let form: [String] = build_form_from_json(semantic_form_json, lang_code)
return realize(form)
}
-7
View File
@@ -1,7 +0,0 @@
// auto-generated by elc --emit-header — do not edit
extern fn sem_get(json: String, key: String) -> String
extern fn generate_frame(frame: [String]) -> String
extern fn generate_frame_lang(frame: [String], lang_code: String) -> String
extern fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [String]
extern fn generate(semantic_form_json: String) -> String
extern fn generate_lang(semantic_form_json: String, lang_code: String) -> String
-2
View File
@@ -1,2 +0,0 @@
import "language-profile.el"
extern fn fn_a(x: String) -> String
-2
View File
@@ -1,2 +0,0 @@
import "language-profile.el"
extern fn fn_b(x: String) -> String
-555
View File
@@ -1,555 +0,0 @@
// grammar.el - Grammar engine: syntactic structure, word order, phrase assembly.
//
// Language-specific word order and question strategy are driven by the language
// profile, not hardcoded. The slot map format (GramSpec) is universal; a "lang"
// key carries the ISO 639-1 code so every downstream function can resolve the
// active profile.
//
// GramSpec slot keys:
// intent - "assert" | "question" | "command"
// agent - subject referent string
// predicate - verb base form
// patient - object noun phrase (optional)
// location - prepositional phrase (optional)
// tense - "present" | "past" | "future"
// aspect - "simple" | "progressive" | "perfect"
// lang - ISO 639-1 code (default "en")
// verb_surf - conjugated verb surface form (computed)
// aux_surf - auxiliary surface form (computed)
//
// Depends on: language-profile
// Slot map helpers
fn slots_get(slots: [String], key: String) -> String {
let n: Int = native_list_len(slots)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(slots, i)
if str_eq(k, key) {
return native_list_get(slots, i + 1)
}
let i = i + 2
}
return ""
}
fn slots_set(slots: [String], key: String, val: String) -> [String] {
let n: Int = native_list_len(slots)
let result: [String] = native_list_empty()
let found: Bool = false
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(slots, i)
let v: String = native_list_get(slots, i + 1)
if str_eq(k, key) {
let result = native_list_append(result, k)
let result = native_list_append(result, val)
let found = true
} else {
let result = native_list_append(result, k)
let result = native_list_append(result, v)
}
let i = i + 2
}
if !found {
let result = native_list_append(result, key)
let result = native_list_append(result, val)
}
return result
}
fn make_slots(k0: String, v0: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, k0)
let r = native_list_append(r, v0)
return r
}
fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String] {
let r: [String] = make_slots(k0, v0)
let r = native_list_append(r, k1)
let r = native_list_append(r, v1)
return r
}
fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String] {
let r: [String] = make_slots2(k0, v0, k1, v1)
let r = native_list_append(r, k2)
let r = native_list_append(r, v2)
return r
}
fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String] {
let r: [String] = make_slots3(k0, v0, k1, v1, k2, v2)
let r = native_list_append(r, k3)
let r = native_list_append(r, v3)
return r
}
fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String] {
let r: [String] = make_slots4(k0, v0, k1, v1, k2, v2, k3, v3)
let r = native_list_append(r, k4)
let r = native_list_append(r, v4)
return r
}
// Grammar rule catalog
fn rule_id(rule: [String]) -> String {
return native_list_get(rule, 0)
}
fn rule_lhs(rule: [String]) -> String {
return native_list_get(rule, 1)
}
fn rule_rhs_len(rule: [String]) -> Int {
let n: Int = native_list_len(rule)
return n - 2
}
fn rule_rhs(rule: [String], idx: Int) -> String {
return native_list_get(rule, idx + 2)
}
fn make_rule(id: String, lhs: String, r0: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, id)
let r = native_list_append(r, lhs)
let r = native_list_append(r, r0)
return r
}
fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String] {
let r: [String] = make_rule(id, lhs, r0)
let r = native_list_append(r, r1)
return r
}
fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String] {
let r: [String] = make_rule2(id, lhs, r0, r1)
let r = native_list_append(r, r2)
return r
}
fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String] {
let r: [String] = make_rule3(id, lhs, r0, r1, r2)
let r = native_list_append(r, r3)
return r
}
fn build_rules() -> [[String]] {
let rules: [[String]] = native_list_empty()
let rules = native_list_append(rules, make_rule2("S-DECL", "S", "NP", "VP"))
let rules = native_list_append(rules, make_rule3("S-QUEST", "S", "Aux", "NP", "VP"))
let rules = native_list_append(rules, make_rule("S-IMP", "S", "VP"))
let rules = native_list_append(rules, make_rule2("NP-DET-N", "NP", "Det", "N"))
let rules = native_list_append(rules, make_rule3("NP-DET-ADJ-N","NP", "Det", "Adj", "N"))
let rules = native_list_append(rules, make_rule("NP-PRON", "NP", "Pron"))
let rules = native_list_append(rules, make_rule("NP-N", "NP", "N"))
let rules = native_list_append(rules, make_rule("VP-V", "VP", "V"))
let rules = native_list_append(rules, make_rule2("VP-V-NP", "VP", "V", "NP"))
let rules = native_list_append(rules, make_rule2("VP-V-PP", "VP", "V", "PP"))
let rules = native_list_append(rules, make_rule3("VP-V-NP-PP", "VP", "V", "NP", "PP"))
let rules = native_list_append(rules, make_rule2("VP-AUX-V", "VP", "Aux", "V"))
let rules = native_list_append(rules, make_rule3("VP-AUX-V-NP", "VP", "Aux", "V", "NP"))
let rules = native_list_append(rules, make_rule2("PP-P-NP", "PP", "P", "NP"))
return rules
}
fn get_rules() -> [[String]] {
return build_rules()
}
fn find_rule(rule_id_str: String) -> [String] {
let rules: [[String]] = get_rules()
let n: Int = native_list_len(rules)
let i: Int = 0
while i < n {
let rule: [String] = native_list_get(rules, i)
let id: String = native_list_get(rule, 0)
if str_eq(id, rule_id_str) {
return rule
}
let i = i + 1
}
let empty: [String] = native_list_empty()
return empty
}
// Tree node construction
fn make_leaf(label: String, word: String) -> String {
return "(" + label + " " + word + ")"
}
fn make_node1(label: String, child0: String) -> String {
return "(" + label + " _ " + child0 + ")"
}
fn make_node2(label: String, child0: String, child1: String) -> String {
return "(" + label + " _ " + child0 + " " + child1 + ")"
}
fn make_node3(label: String, child0: String, child1: String, child2: String) -> String {
return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + ")"
}
fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String {
return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + " " + child3 + ")"
}
// Tree rendering
fn nlg_is_ws(c: String) -> Bool {
if str_eq(c, " ") { return true }
if str_eq(c, "\t") { return true }
if str_eq(c, "\n") { return true }
return false
}
fn skip_ws(s: String, pos: Int) -> Int {
let n: Int = str_len(s)
let i: Int = pos
let running: Bool = true
while running {
if i >= n {
let running = false
} else {
let c: String = str_slice(s, i, i + 1)
if nlg_is_ws(c) {
let i = i + 1
} else {
let running = false
}
}
}
return i
}
fn scan_token(s: String, start: Int) -> [String] {
let n: Int = str_len(s)
let i: Int = start
let running: Bool = true
while running {
if i >= n {
let running = false
} else {
let c: String = str_slice(s, i, i + 1)
if nlg_is_ws(c) {
let running = false
} else {
if str_eq(c, "(") {
let running = false
} else {
if str_eq(c, ")") {
let running = false
} else {
let i = i + 1
}
}
}
}
}
let tok: String = str_slice(s, start, i)
let result: [String] = native_list_empty()
let result = native_list_append(result, tok)
let result = native_list_append(result, int_to_str(i))
return result
}
fn render_tree(tree: String) -> String {
let words: [String] = native_list_empty()
let n: Int = str_len(tree)
let i: Int = 0
let prev_was_open: Bool = false
while i < n {
let c: String = str_slice(tree, i, i + 1)
if str_eq(c, "(") {
let prev_was_open = true
let i = i + 1
} else {
if str_eq(c, ")") {
let prev_was_open = false
let i = i + 1
} else {
if nlg_is_ws(c) {
let i = i + 1
} else {
let tok_info: [String] = scan_token(tree, i)
let tok: String = native_list_get(tok_info, 0)
let new_i: Int = str_to_int(native_list_get(tok_info, 1))
let i = new_i
if prev_was_open {
let prev_was_open = false
} else {
if !str_eq(tok, "_") {
let words = native_list_append(words, tok)
}
}
}
}
}
}
return str_join(words, " ")
}
// Word-order engine
// gram_word_order: returns the word order string from a profile.
fn gram_word_order(profile: [String]) -> String {
return lang_word_order(profile)
}
// gram_order_constituents: order Subject, Verb, Object tokens according to the
// language profile's word_order.
//
// subj, verb, obj: surface strings (may be empty).
// Returns a space-joined string in the correct order.
//
// Supported orders: SVO, SOV, VSO, VOS, OVS, OSV, free (defaults to SVO).
fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String {
let order: String = gram_word_order(profile)
let parts: [String] = native_list_empty()
if str_eq(order, "SVO") {
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
return str_join(parts, " ")
}
if str_eq(order, "SOV") {
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
return str_join(parts, " ")
}
if str_eq(order, "VSO") {
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
return str_join(parts, " ")
}
if str_eq(order, "VOS") {
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
return str_join(parts, " ")
}
if str_eq(order, "OVS") {
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
return str_join(parts, " ")
}
if str_eq(order, "OSV") {
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
return str_join(parts, " ")
}
// "free" and unknown: use SVO as the neutral citation order.
if !str_eq(subj, "") { let parts = native_list_append(parts, subj) }
if !str_eq(verb, "") { let parts = native_list_append(parts, verb) }
if !str_eq(obj, "") { let parts = native_list_append(parts, obj) }
return str_join(parts, " ")
}
// gram_build_vp: construct a verb phrase surface string.
//
// verb: main verb surface form.
// aux: auxiliary surface form (empty if none).
// profile: language profile.
//
// In SVO/VSO/VOS languages the auxiliary precedes the main verb.
// In SOV languages the verb cluster appears at the end; we keep aux before V
// as a reasonable default for the auxiliary-final constructions in those languages.
fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String {
if str_eq(aux, "") {
return verb
}
return aux + " " + verb
}
// gram_question_strategy: returns the question formation strategy for a language.
//
// "do-support" - English: "Do you see?" do-auxiliary inserted, verb stays base
// "particle" - Japanese: sentence-final appended
// "intonation" - Mandarin, Spanish: rising intonation only, word order unchanged
// "inversion" - French, German: subject-verb inversion
fn gram_question_strategy(profile: [String]) -> String {
let code: String = lang_get(profile, "code")
if str_eq(code, "en") { return "do-support" }
if str_eq(code, "ja") { return "particle" }
if str_eq(code, "zh") { return "intonation" }
if str_eq(code, "es") { return "intonation" }
if str_eq(code, "fr") { return "inversion" }
if str_eq(code, "de") { return "inversion" }
if str_eq(code, "ar") { return "intonation" }
if str_eq(code, "hi") { return "particle" }
if str_eq(code, "ru") { return "intonation" }
if str_eq(code, "fi") { return "particle" }
if str_eq(code, "sw") { return "intonation" }
if str_eq(code, "la") { return "intonation" } // Latin: word order marks Q (VSO or -ne suffix)
if str_eq(code, "he") { return "intonation" } // Modern Hebrew: rising intonation
if str_eq(code, "grc") { return "intonation" } // Ancient Greek: ἆρα particle or intonation
if str_eq(code, "ang") { return "intonation" } // Old English: hwæþer particle or intonation
if str_eq(code, "sa") { return "intonation" } // Sanskrit: kim particle or intonation
if str_eq(code, "got") { return "intonation" } // Gothic: ibai particle or intonation
if str_eq(code, "non") { return "intonation" } // Old Norse: hvárr particle or intonation
if str_eq(code, "enm") { return "do-support" } // Middle English: do-support emerging
if str_eq(code, "pi") { return "intonation" } // Pali: kim particle or intonation
// Unknown: default to intonation (safest never wrong, just flat)
return "intonation"
}
// NP and PP assembly
//
// These functions are profile-aware but the logic is the same across languages
// because we work with pre-assembled strings (Engram vocabulary supplies
// language-specific forms before these functions see them).
fn is_pronoun(word: String) -> Bool {
if str_eq(word, "I") { return true }
if str_eq(word, "you") { return true }
if str_eq(word, "he") { return true }
if str_eq(word, "she") { return true }
if str_eq(word, "it") { return true }
if str_eq(word, "we") { return true }
if str_eq(word, "they") { return true }
if str_eq(word, "me") { return true }
if str_eq(word, "him") { return true }
if str_eq(word, "her") { return true }
if str_eq(word, "us") { return true }
if str_eq(word, "them") { return true }
return false
}
// build_np: assemble a noun phrase tree from a referent string.
// profile parameter reserved for future case-marking / article agreement.
fn build_np(referent: String, slots: [String]) -> String {
if is_pronoun(referent) {
return make_node1("NP", make_leaf("Pron", referent))
}
let parts: [String] = str_split(referent, " ")
let np: Int = native_list_len(parts)
if np == 1 {
return make_node1("NP", make_leaf("N", referent))
}
if np == 2 {
let det: String = native_list_get(parts, 0)
let noun: String = native_list_get(parts, 1)
return make_node2("NP", make_leaf("Det", det), make_leaf("N", noun))
}
if np == 3 {
let det: String = native_list_get(parts, 0)
let adj: String = native_list_get(parts, 1)
let noun: String = native_list_get(parts, 2)
return make_node3("NP", make_leaf("Det", det), make_leaf("Adj", adj), make_leaf("N", noun))
}
return make_node1("NP", make_leaf("N", referent))
}
// build_pp: assemble a prepositional phrase tree from a "PREP NP" string.
// For postpositional languages (ja, hi, ko) the slot value is expected to be
// already pre-assembled with the postposition in the correct position by the
// caller (vocabulary lookup from Engram supplies the right surface form).
fn build_pp(loc: String) -> String {
let parts: [String] = str_split(loc, " ")
let n: Int = native_list_len(parts)
if n < 2 {
return make_leaf("PP", loc)
}
let prep: String = native_list_get(parts, 0)
let np_parts: [String] = native_list_empty()
let i: Int = 1
while i < n {
let np_parts = native_list_append(np_parts, native_list_get(parts, i))
let i = i + 1
}
let np_str: String = str_join(np_parts, " ")
let np_tree: String = build_np(np_str, native_list_empty())
return make_node2("PP", make_leaf("P", prep), np_tree)
}
// VP tree construction
fn build_vp_body(slots: [String]) -> String {
let verb_surf: String = slots_get(slots, "verb_surf")
let patient: String = slots_get(slots, "patient")
let loc: String = slots_get(slots, "location")
if !str_eq(patient, "") {
let obj_np: String = build_np(patient, slots)
if !str_eq(loc, "") {
let pp: String = build_pp(loc)
return make_node3("VP", make_leaf("V", verb_surf), obj_np, pp)
}
return make_node2("VP", make_leaf("V", verb_surf), obj_np)
}
if !str_eq(loc, "") {
let pp: String = build_pp(loc)
return make_node2("VP", make_leaf("V", verb_surf), pp)
}
return make_node1("VP", make_leaf("V", verb_surf))
}
fn build_vp_from_slots(slots: [String]) -> String {
let aux_surf: String = slots_get(slots, "aux_surf")
if !str_eq(aux_surf, "") {
let verb_surf: String = slots_get(slots, "verb_surf")
let patient: String = slots_get(slots, "patient")
let loc: String = slots_get(slots, "location")
if !str_eq(patient, "") {
let obj_np: String = build_np(patient, slots)
return make_node3("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf), obj_np)
}
return make_node2("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf))
}
return build_vp_body(slots)
}
// Tree generator
fn generate_tree(rule_id_str: String, slots: [String]) -> String {
let rule: [String] = find_rule(rule_id_str)
let n: Int = native_list_len(rule)
if n == 0 {
return make_leaf("ERR", "unknown-rule")
}
let lhs: String = native_list_get(rule, 1)
if str_eq(rule_id_str, "S-DECL") {
let agent: String = slots_get(slots, "agent")
let np_tree: String = build_np(agent, slots)
let vp_tree: String = build_vp_from_slots(slots)
return make_node2("S", np_tree, vp_tree)
}
if str_eq(rule_id_str, "S-QUEST") {
let agent: String = slots_get(slots, "agent")
let np_tree: String = build_np(agent, slots)
let vp_tree: String = build_vp_body(slots)
let aux_surf: String = slots_get(slots, "aux_surf")
return make_node3("S", make_leaf("Aux", aux_surf), np_tree, vp_tree)
}
if str_eq(rule_id_str, "S-IMP") {
let vp_tree: String = build_vp_from_slots(slots)
return make_node1("S", vp_tree)
}
return make_leaf(lhs, "?")
}
-38
View File
@@ -1,38 +0,0 @@
// auto-generated by elc --emit-header — do not edit
extern fn slots_get(slots: [String], key: String) -> String
extern fn slots_set(slots: [String], key: String, val: String) -> [String]
extern fn make_slots(k0: String, v0: String) -> [String]
extern fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String]
extern fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String]
extern fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String]
extern fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String]
extern fn rule_id(rule: [String]) -> String
extern fn rule_lhs(rule: [String]) -> String
extern fn rule_rhs_len(rule: [String]) -> Int
extern fn rule_rhs(rule: [String], idx: Int) -> String
extern fn make_rule(id: String, lhs: String, r0: String) -> [String]
extern fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String]
extern fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String]
extern fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String]
extern fn build_rules() -> [[String]]
extern fn get_rules() -> [[String]]
extern fn find_rule(rule_id_str: String) -> [String]
extern fn make_leaf(label: String, word: String) -> String
extern fn make_node1(label: String, child0: String) -> String
extern fn make_node2(label: String, child0: String, child1: String) -> String
extern fn make_node3(label: String, child0: String, child1: String, child2: String) -> String
extern fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String
extern fn nlg_is_ws(c: String) -> Bool
extern fn skip_ws(s: String, pos: Int) -> Int
extern fn scan_token(s: String, start: Int) -> [String]
extern fn render_tree(tree: String) -> String
extern fn gram_word_order(profile: [String]) -> String
extern fn gram_order_constituents(subj: String, verb: String, obj: String, profile: [String]) -> String
extern fn gram_build_vp(verb: String, aux: String, profile: [String]) -> String
extern fn gram_question_strategy(profile: [String]) -> String
extern fn is_pronoun(word: String) -> Bool
extern fn build_np(referent: String, slots: [String]) -> String
extern fn build_pp(loc: String) -> String
extern fn build_vp_body(slots: [String]) -> String
extern fn build_vp_from_slots(slots: [String]) -> String
extern fn generate_tree(rule_id_str: String, slots: [String]) -> String
-65
View File
@@ -1,65 +0,0 @@
// image-demo.el - Drive the native PNG surface: plan a scene from a small
// meaning phrase (incl. a NEG frame) and emit a byte-valid 64x64 PNG whose
// palette is read from elp/faculty/sig/scene.basis.
fn img_frame(relation: String, polarity: String, confidence: String, importance: String, salience: String, subj_id: String) -> [String] {
let f: [String] = native_list_empty()
let f: [String] = native_list_append(f, "relation")
let f: [String] = native_list_append(f, relation)
let f: [String] = native_list_append(f, "polarity")
let f: [String] = native_list_append(f, polarity)
let f: [String] = native_list_append(f, "confidence")
let f: [String] = native_list_append(f, confidence)
let f: [String] = native_list_append(f, "importance")
let f: [String] = native_list_append(f, importance)
let f: [String] = native_list_append(f, "salience")
let f: [String] = native_list_append(f, salience)
let f: [String] = native_list_append(f, "subj_id")
let f: [String] = native_list_append(f, subj_id)
return f
}
fn rgb_str(c: [Int]) -> String {
return int_to_str(native_list_get(c, 0)) + "," + int_to_str(native_list_get(c, 1)) + "," + int_to_str(native_list_get(c, 2))
}
fn run_image() -> Int {
fs_mkdir("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out")
let table: [Int] = crc_table()
println("crc_table[1]=" + int_to_str(native_list_get(table, 1)) + " (expect 1996959894 / 0x77073096)")
let basis: [String] = basis_load("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/sig/scene.basis")
let warm: [Int] = parse_rgb(basis_field(basis, "warm"))
let cool: [Int] = parse_rgb(basis_field(basis, "cool"))
let bg: [Int] = parse_rgb(basis_field(basis, "bg"))
println("basis warm=" + rgb_str(warm) + " cool=" + rgb_str(cool) + " bg=" + rgb_str(bg) + " (read from scene.basis)")
let frames: [[String]] = native_list_empty()
let frames: [[String]] = native_list_append(frames, img_frame("agent", "aff", "0.9", "0.8", "0", "s1"))
let frames: [[String]] = native_list_append(frames, img_frame("theme", "aff", "0.7", "0.6", "1", "s2"))
let frames: [[String]] = native_list_append(frames, img_frame("cause", "aff", "0.8", "0.9", "0", "s3"))
let frames: [[String]] = native_list_append(frames, img_frame("negation", "neg", "0.85", "0.7", "1", "s4"))
let frames: [[String]] = native_list_append(frames, img_frame("goal", "aff", "0.6", "0.5", "0", "s5"))
let frames: [[String]] = native_list_append(frames, img_frame("result", "aff", "0.95", "1.0", "1", "s6"))
let shapes: [[Int]] = plan_scene(frames, warm, cool)
let ns: Int = native_list_len(shapes)
println("planned " + int_to_str(ns) + " shapes:")
let si: Int = 0
while si < ns {
let sh: [Int] = native_list_get(shapes, si)
let pol: String = surface_get(native_list_get(frames, si), "polarity")
println(" shape " + int_to_str(si) + " type=" + int_to_str(native_list_get(sh, 0)) + " x=" + int_to_str(native_list_get(sh, 1)) + " y=" + int_to_str(native_list_get(sh, 2)) + " size=" + int_to_str(native_list_get(sh, 3)) + " rgb=" + int_to_str(native_list_get(sh, 4)) + "," + int_to_str(native_list_get(sh, 5)) + "," + int_to_str(native_list_get(sh, 6)) + " polarity=" + pol)
let si: Int = si + 1
}
let raw: [Int] = rasterize(64, 64, shapes, bg)
println("rasterized raw (filtered scanlines) bytes=" + int_to_str(native_list_len(raw)) + " (expect 12352)")
let png: [Int] = png_build(64, 64, raw, table)
let plen: Int = native_list_len(png)
let ok: Int = png_write("/Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/scene.png", png)
println("PNG bytes=" + int_to_str(plen) + " -> /Users/will/Development/neuron-technologies/foundation/el/.claude/worktrees/agent-aaf04b0a9714c4070/elp/faculty/out/scene.png (write_ok=" + int_to_str(ok) + ")")
return plen
}
println("image-demo returned png_bytes=" + int_to_str(run_image()))
-412
View File
@@ -1,412 +0,0 @@
// image-surface.el - Native own-core raster PNG surface (the image efferent
// twin of audio). Renders a 64x64 RGB scene deterministically from a frame's
// meaning-geometry, then serialises a byte-valid PNG entirely own-core:
// 8-byte magic, IHDR, IDAT (zlib STORED/uncompressed DEFLATE + Adler32), IEND,
// with a per-chunk CRC32 computed via software xor32 (EL has no bitwise ops).
//
// The RGB palette basis is read from elp/faculty/sig/scene.basis (data, not
// literals) - the same read-from-learned discipline as the audio signatures.
// Integer-only throughout; pixels are composed functionally (painter's order)
// so no list mutation is needed.
// -- small int/parse helpers (self-contained) ----------------------------------
fn i_str_to_int(s: String) -> Int {
let n: Int = str_len(s)
let i: Int = 0
let v: Int = 0
while i < n {
let c: Int = str_char_code(s, i)
if c >= 48 {
if c < 58 {
let v: Int = v * 10 + (c - 48)
}
}
let i: Int = i + 1
}
return v
}
fn basis_load(path: String) -> [String] {
return str_split(fs_read(path), "\n")
}
fn basis_field(lines: [String], key: String) -> String {
let pref: String = key + ": "
let n: Int = native_list_len(lines)
let plen: Int = str_len(pref)
let i: Int = 0
while i < n {
let ln: String = native_list_get(lines, i)
if str_starts_with(ln, pref) {
return str_slice(ln, plen, str_len(ln))
}
let i: Int = i + 1
}
return ""
}
fn parse_rgb(csv: String) -> [Int] {
let parts: [String] = str_split(csv, ",")
let out: [Int] = native_list_empty()
let n: Int = native_list_len(parts)
let i: Int = 0
while i < n {
let v: Int = i_str_to_int(native_list_get(parts, i))
let out: [Int] = native_list_append(out, v)
let i: Int = i + 1
}
return out
}
// -- software 32-bit XOR (no bitwise ops in EL) --------------------------------
fn xor32(a: Int, b: Int) -> Int {
let r: Int = 0
let bit: Int = 1
let i: Int = 0
while i < 32 {
let abit: Int = (a / bit) % 2
let bbit: Int = (b / bit) % 2
if abit != bbit {
let add: Int = bit
let r: Int = r + add
}
let bit: Int = bit * 2
let i: Int = i + 1
}
return r
}
// -- CRC32 (table-driven, table built with xor32) ------------------------------
fn crc_table() -> [Int] {
let t: [Int] = native_list_empty()
let n: Int = 0
while n < 256 {
let c: Int = n
let k: Int = 0
while k < 8 {
if c % 2 == 1 {
let h: Int = c / 2
let c: Int = xor32(h, 3988292384)
} else {
let c: Int = c / 2
}
let k: Int = k + 1
}
let t: [Int] = native_list_append(t, c)
let n: Int = n + 1
}
return t
}
fn crc32_of(bytes: [Int], table: [Int]) -> Int {
let crc: Int = 4294967295
let n: Int = native_list_len(bytes)
let i: Int = 0
while i < n {
let b: Int = native_list_get(bytes, i)
let lo: Int = crc % 256
let idx: Int = xor32(lo, b) % 256
let tv: Int = native_list_get(table, idx)
let hi: Int = crc / 256
let crc: Int = xor32(hi, tv)
let i: Int = i + 1
}
return xor32(crc, 4294967295)
}
// -- Adler32 (for the zlib trailer) --------------------------------------------
fn adler32_of(bytes: [Int]) -> Int {
let a: Int = 1
let b: Int = 0
let n: Int = native_list_len(bytes)
let i: Int = 0
while i < n {
let byte: Int = native_list_get(bytes, i)
let a: Int = (a + byte) % 65521
let b: Int = (b + a) % 65521
let i: Int = i + 1
}
return b * 65536 + a
}
// -- byte-list append helpers --------------------------------------------------
fn app_u32be(dst: [Int], v: Int) -> [Int] {
let dst: [Int] = native_list_append(dst, (v / 16777216) % 256)
let dst: [Int] = native_list_append(dst, (v / 65536) % 256)
let dst: [Int] = native_list_append(dst, (v / 256) % 256)
let dst: [Int] = native_list_append(dst, v % 256)
return dst
}
fn app_tag(dst: [Int], s: String) -> [Int] {
let n: Int = str_len(s)
let i: Int = 0
while i < n {
let dst: [Int] = native_list_append(dst, str_char_code(s, i))
let i: Int = i + 1
}
return dst
}
fn app_all(dst: [Int], src: [Int]) -> [Int] {
let n: Int = native_list_len(src)
let i: Int = 0
while i < n {
let dst: [Int] = native_list_append(dst, native_list_get(src, i))
let i: Int = i + 1
}
return dst
}
// -- plan: frame meaning-geometry -> shape atoms -------------------------------
// shape = [type, x, y, size, r, g, b] (type 0=rect 1=disc 2=triangle)
fn charsum(s: String) -> Int {
let n: Int = str_len(s)
let i: Int = 0
let acc: Int = 0
while i < n {
let c: Int = str_char_code(s, i)
let acc: Int = acc + c
let i: Int = i + 1
}
return acc
}
fn micro_of(s: String) -> Int {
let dot: Int = str_index_of(s, ".")
if dot < 0 { return i_str_to_int(s) * 1000000 }
let n: Int = str_len(s)
let fp: String = str_slice(s, dot + 1, n)
let ip: String = str_slice(s, 0, dot)
let iv: Int = i_str_to_int(ip)
let fv: Int = 0
let scale: Int = 100000
let fl: Int = str_len(fp)
let i: Int = 0
while i < 6 {
let d: Int = 0
if i < fl { let d: Int = str_char_code(fp, i) - 48 }
let fv: Int = fv + d * scale
let scale: Int = scale / 10
let i: Int = i + 1
}
return iv * 1000000 + fv
}
fn plan_scene(frames: [[String]], warm: [Int], cool: [Int]) -> [[Int]] {
let shapes: [[Int]] = native_list_empty()
let nf: Int = native_list_len(frames)
let fi: Int = 0
while fi < nf {
let fr: [String] = native_list_get(frames, fi)
let relation: String = surface_get(fr, "relation")
let polarity: String = surface_get(fr, "polarity")
let confidence: String = surface_get(fr, "confidence")
let importance: String = surface_get(fr, "importance")
let salience: String = surface_get(fr, "salience")
// relation -> shape type
let stype: Int = charsum(relation) % 3
// confidence -> size (8..22)
let cmi: Int = micro_of(confidence)
let size: Int = 8 + cmi / 71428
// salience -> y
let sal: Int = i_str_to_int(salience)
let y: Int = 6 + sal * 26
// subj_id/index -> x
let x: Int = 4 + (fi * 10) % 48
// polarity -> warm/cool base color
let br: Int = native_list_get(warm, 0)
let bg2: Int = native_list_get(warm, 1)
let bb: Int = native_list_get(warm, 2)
if str_eq(polarity, "neg") {
let br: Int = native_list_get(cool, 0)
let bg2: Int = native_list_get(cool, 1)
let bb: Int = native_list_get(cool, 2)
}
// importance -> brightness (500..1000 permille)
let imi: Int = micro_of(importance)
let bpm: Int = 500 + imi / 2000
let r: Int = br * bpm / 1000
let g: Int = bg2 * bpm / 1000
let b: Int = bb * bpm / 1000
let sh: [Int] = native_list_empty()
let sh: [Int] = native_list_append(sh, stype)
let sh: [Int] = native_list_append(sh, x)
let sh: [Int] = native_list_append(sh, y)
let sh: [Int] = native_list_append(sh, size)
let sh: [Int] = native_list_append(sh, r)
let sh: [Int] = native_list_append(sh, g)
let sh: [Int] = native_list_append(sh, b)
let shapes: [[Int]] = native_list_append(shapes, sh)
let fi: Int = fi + 1
}
return shapes
}
// covers: is (px,py) inside this shape?
fn covers(sh: [Int], px: Int, py: Int) -> Bool {
let stype: Int = native_list_get(sh, 0)
let sx: Int = native_list_get(sh, 1)
let sy: Int = native_list_get(sh, 2)
let size: Int = native_list_get(sh, 3)
let cx: Int = sx + size / 2
if stype == 0 {
if px >= sx {
if px < sx + size {
if py >= sy {
if py < sy + size {
return true
}
}
}
}
return false
}
if stype == 1 {
let rad: Int = size / 2
let dx: Int = px - cx
let dy: Int = py - (sy + rad)
if dx * dx + dy * dy <= rad * rad {
return true
}
return false
}
// triangle: apex at top (sy), base at sy+size
if py >= sy {
if py < sy + size {
let dyv: Int = py - sy
let halfw: Int = dyv / 2
let dxv: Int = px - cx
let adx: Int = dxv
if adx < 0 { let adx: Int = 0 - dxv }
if adx <= halfw {
return true
}
}
}
return false
}
// pixel_color: painter's algorithm - last covering shape wins. Returns [r,g,b].
fn pixel_color(px: Int, py: Int, shapes: [[Int]], bg: [Int]) -> [Int] {
let r: Int = native_list_get(bg, 0)
let g: Int = native_list_get(bg, 1)
let b: Int = native_list_get(bg, 2)
let n: Int = native_list_len(shapes)
let i: Int = 0
while i < n {
let sh: [Int] = native_list_get(shapes, i)
if covers(sh, px, py) {
let r: Int = native_list_get(sh, 4)
let g: Int = native_list_get(sh, 5)
let b: Int = native_list_get(sh, 6)
}
let i: Int = i + 1
}
let out: [Int] = native_list_empty()
let out: [Int] = native_list_append(out, r)
let out: [Int] = native_list_append(out, g)
let out: [Int] = native_list_append(out, b)
return out
}
// rasterize: build the raw (filtered) scanline byte stream, filter byte 0 / row.
fn rasterize(w: Int, h: Int, shapes: [[Int]], bg: [Int]) -> [Int] {
let raw: [Int] = native_list_empty()
let y: Int = 0
while y < h {
let raw: [Int] = native_list_append(raw, 0)
let x: Int = 0
while x < w {
let col: [Int] = pixel_color(x, y, shapes, bg)
let raw: [Int] = native_list_append(raw, native_list_get(col, 0))
let raw: [Int] = native_list_append(raw, native_list_get(col, 1))
let raw: [Int] = native_list_append(raw, native_list_get(col, 2))
let x: Int = x + 1
}
let y: Int = y + 1
}
return raw
}
// zlib stream with a single STORED (uncompressed) DEFLATE block + Adler32.
fn zlib_store(raw: [Int]) -> [Int] {
let z: [Int] = native_list_empty()
let z: [Int] = native_list_append(z, 120)
let z: [Int] = native_list_append(z, 1)
let z: [Int] = native_list_append(z, 1)
let len: Int = native_list_len(raw)
let nlen: Int = 65535 - len
let z: [Int] = native_list_append(z, len % 256)
let z: [Int] = native_list_append(z, (len / 256) % 256)
let z: [Int] = native_list_append(z, nlen % 256)
let z: [Int] = native_list_append(z, (nlen / 256) % 256)
let z: [Int] = app_all(z, raw)
let ad: Int = adler32_of(raw)
let z: [Int] = app_u32be(z, ad)
return z
}
// append a full PNG chunk: length + (type+data) + crc32(type+data).
fn app_chunk(png: [Int], type_and_data: [Int], table: [Int]) -> [Int] {
let total: Int = native_list_len(type_and_data)
let dlen: Int = total - 4
let png: [Int] = app_u32be(png, dlen)
let png: [Int] = app_all(png, type_and_data)
let crc: Int = crc32_of(type_and_data, table)
let png: [Int] = app_u32be(png, crc)
return png
}
fn png_build(w: Int, h: Int, raw: [Int], table: [Int]) -> [Int] {
let png: [Int] = native_list_empty()
// 8-byte signature
let png: [Int] = native_list_append(png, 137)
let png: [Int] = native_list_append(png, 80)
let png: [Int] = native_list_append(png, 78)
let png: [Int] = native_list_append(png, 71)
let png: [Int] = native_list_append(png, 13)
let png: [Int] = native_list_append(png, 10)
let png: [Int] = native_list_append(png, 26)
let png: [Int] = native_list_append(png, 10)
// IHDR
let ihdr: [Int] = native_list_empty()
let ihdr: [Int] = app_tag(ihdr, "IHDR")
let ihdr: [Int] = app_u32be(ihdr, w)
let ihdr: [Int] = app_u32be(ihdr, h)
let ihdr: [Int] = native_list_append(ihdr, 8)
let ihdr: [Int] = native_list_append(ihdr, 2)
let ihdr: [Int] = native_list_append(ihdr, 0)
let ihdr: [Int] = native_list_append(ihdr, 0)
let ihdr: [Int] = native_list_append(ihdr, 0)
let png: [Int] = app_chunk(png, ihdr, table)
// IDAT
let z: [Int] = zlib_store(raw)
let idat: [Int] = native_list_empty()
let idat: [Int] = app_tag(idat, "IDAT")
let idat: [Int] = app_all(idat, z)
let png: [Int] = app_chunk(png, idat, table)
// IEND
let iend: [Int] = native_list_empty()
let iend: [Int] = app_tag(iend, "IEND")
let png: [Int] = app_chunk(png, iend, table)
return png
}
fn png_write(path: String, png: [Int]) -> Int {
let n: Int = native_list_len(png)
let buf: String = __str_alloc(n)
let i: Int = 0
while i < n {
let buf: String = __str_set_char(buf, i, native_list_get(png, i))
let i: Int = i + 1
}
let ok: Int = fs_write_bytes(path, buf, n)
return ok
}
-761
View File
@@ -1,761 +0,0 @@
// big language-profile for testing
fn lang_profile_big0(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big0(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big0("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big1(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big1(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big1("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big2(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big2(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big2("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big3(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big3(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big3("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big4(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big4(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big4("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big5(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big5(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big5("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big6(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big6(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big6("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big7(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big7(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big7("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big8(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big8(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big8("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big9(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big9(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big9("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big10(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big10(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big10("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big11(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big11(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big11("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big12(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big12(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big12("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big13(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big13(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big13("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big14(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big14(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big14("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big15(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big15(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big15("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big16(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big16(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big16("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big17(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big17(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big17("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big18(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big18(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big18("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
fn lang_profile_big19(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
fn lang_get_big19(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
fn lang_profile_en() -> [String] {
return lang_profile_big19("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
-353
View File
@@ -1,353 +0,0 @@
// language-profile.el - Language profile data and accessors.
//
// A language profile is a slot map ([String] key-value list) describing the
// typological properties of a natural language. The engine reads these
// properties to drive morphology, word-order, and question-formation without
// any per-language code paths.
//
// Profile slot keys:
// code - ISO 639-1 code: "en", "ja", "ar", "zh", "de", "fr", "es", "sw", "hi", "ru", etc.
// word_order - "SVO" | "SOV" | "VSO" | "VOS" | "OVS" | "OSV" | "free"
// morph_type - "isolating" | "agglutinative" | "fusional" | "polysynthetic"
// has_case - "true" | "false"
// has_gender - "true" | "false"
// script_dir - "ltr" | "rtl" | "ttb"
// agreement - semicolon-separated features: "number;person" | "number;person;gender;case" | "none"
// null_subject - "true" | "false" (pro-drop: subject may be omitted)
// Constructor
fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String] {
let r: [String] = native_list_empty()
let r = native_list_append(r, "code")
let r = native_list_append(r, code)
let r = native_list_append(r, "word_order")
let r = native_list_append(r, word_order)
let r = native_list_append(r, "morph_type")
let r = native_list_append(r, morph_type)
let r = native_list_append(r, "has_case")
let r = native_list_append(r, has_case)
let r = native_list_append(r, "has_gender")
let r = native_list_append(r, has_gender)
let r = native_list_append(r, "script_dir")
let r = native_list_append(r, script_dir)
let r = native_list_append(r, "agreement")
let r = native_list_append(r, agreement)
let r = native_list_append(r, "null_subject")
let r = native_list_append(r, null_subject)
return r
}
// Accessor
fn lang_get(profile: [String], key: String) -> String {
let n: Int = native_list_len(profile)
let i: Int = 0
while i < n - 1 {
let k: String = native_list_get(profile, i)
if str_eq(k, key) {
return native_list_get(profile, i + 1)
}
let i = i + 2
}
return ""
}
// Built-in profiles
//
// Each profile encodes typological facts about one language. These are data,
// not separate code paths. Adding a new language means adding a new profile
// and loading its vocabulary/suffix tables into the Engram - no engine changes.
// English: SVO, fusional, no grammatical case (nominative/accusative collapsed),
// no grammatical gender, left-to-right, agreement on number and person,
// obligatory subject (no pro-drop).
fn lang_profile_en() -> [String] {
return lang_profile("en", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
// Japanese: SOV, agglutinative, grammatical relations marked by postpositions
// (not inflectional case), no grammatical gender, left-to-right, no agreement
// morphology on verbs, pro-drop (null subject frequent).
fn lang_profile_ja() -> [String] {
return lang_profile("ja", "SOV", "agglutinative", "false", "false", "ltr", "none", "true")
}
// Arabic: VSO, fusional, full case system, grammatical gender (masc/fem),
// right-to-left script, agreement on number, person, gender, and case,
// pro-drop (subject agreement marking on verb allows subject omission).
fn lang_profile_ar() -> [String] {
return lang_profile("ar", "VSO", "fusional", "true", "true", "rtl", "number;person;gender;case", "true")
}
// Mandarin Chinese: SVO, isolating (no morphological inflection), no case,
// no grammatical gender, left-to-right, no agreement (no morphological marking),
// null subject allowed in discourse context.
fn lang_profile_zh() -> [String] {
return lang_profile("zh", "SVO", "isolating", "false", "false", "ltr", "none", "true")
}
// German: V2 (second-position verb, base SOV in subordinate clauses), fusional,
// four-case system, three grammatical genders, left-to-right, agreement on
// number, person, gender, and case, obligatory subject.
fn lang_profile_de() -> [String] {
return lang_profile("de", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Spanish: SVO, fusional, no morphological case (but object clitics exist),
// grammatical gender (masc/fem), left-to-right, agreement on number, person,
// and gender, pro-drop (rich verbal agreement allows subject omission).
fn lang_profile_es() -> [String] {
return lang_profile("es", "SVO", "fusional", "false", "true", "ltr", "number;person;gender", "true")
}
// Finnish: SOV, agglutinative, fifteen grammatical cases, no grammatical gender,
// left-to-right, agreement on number, person, and case, no pro-drop (subject
// required in finite clauses).
fn lang_profile_fi() -> [String] {
return lang_profile("fi", "SOV", "agglutinative", "true", "false", "ltr", "number;person;case", "false")
}
// Swahili: SVO, agglutinative, noun-class system (15+ classes replacing gender),
// no case inflection, left-to-right, agreement driven by noun class and number,
// pro-drop (subject prefix on verb can stand alone).
fn lang_profile_sw() -> [String] {
return lang_profile("sw", "SVO", "agglutinative", "false", "false", "ltr", "noun-class;number", "true")
}
// Hindi: SOV, fusional, case-marked postpositional system, grammatical gender
// (masc/fem), left-to-right (Devanagari script still ltr), agreement on number,
// person, gender, and case, pro-drop (subject frequently dropped).
fn lang_profile_hi() -> [String] {
return lang_profile("hi", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Russian: free word order (pragmatically determined), fusional, six-case system,
// three grammatical genders, left-to-right (Cyrillic), agreement on number,
// person, gender, and case, no pro-drop (subject required).
fn lang_profile_ru() -> [String] {
return lang_profile("ru", "free", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// French: SVO, fusional, no morphological case (but clitic object pronouns),
// two grammatical genders (masc/fem), left-to-right, agreement on number,
// person, and gender, no pro-drop.
fn lang_profile_fr() -> [String] {
return lang_profile("fr", "SVO", "fusional", "false", "true", "ltr", "number;person;gender", "false")
}
// Latin: SOV (highly free word order), fusional, six-case system (nom/gen/dat/acc/abl/voc),
// three genders (masc/fem/neut), left-to-right, rich agreement on number, person, gender,
// and case, pro-drop (subject expressed in verb ending).
fn lang_profile_la() -> [String] {
return lang_profile("la", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Hebrew (Modern): SVO, Semitic trilateral root morphology, two genders (masc/fem),
// two numbers (singular/plural; dual vestigial), right-to-left (Hebrew script),
// agreement on number, person, gender; zero copula in present tense; no grammatical cases.
fn lang_profile_he() -> [String] {
return lang_profile("he", "SVO", "semitic", "true", "false", "rtl", "number;person;gender", "true")
}
// Sanskrit: SOV/free, highly fusional, 3 genders, 8 cases, 3 numbers (sg/du/pl),
// Devanagari script, rich verb system (10 classes, 9 tenses/moods), pro-drop.
fn lang_profile_sa() -> [String] {
return lang_profile("sa", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Gothic: SOV, fusional, 3 genders, 4 cases, singular/plural,
// Gothic alphabet (romanized as þ/ƕ/ai/au/ei), strong and weak classes, pro-drop.
fn lang_profile_got() -> [String] {
return lang_profile("got", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Old Norse: free/SOV, fusional, 3 genders, 4 cases, singular/plural,
// definite article as noun suffix (-inn/-in/-it), strong and weak classes, pro-drop.
fn lang_profile_non() -> [String] {
return lang_profile("non", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Middle English (ca. 11001500): SVO emerging, mostly lost case system,
// -es plural/genitive, strong and weak verbs, no grammatical gender on nouns.
fn lang_profile_enm() -> [String] {
return lang_profile("enm", "SVO", "fusional", "false", "false", "ltr", "number;person", "false")
}
// Pali: SOV, fusional (simplified Sanskrit), 3 genders, 8 cases, sg/pl,
// Latin transliteration with IAST diacritics, Buddhist canonical language.
fn lang_profile_pi() -> [String] {
return lang_profile("pi", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Ancient Greek: free/SOV word order, highly fusional, 3 genders, 5 cases (nom/acc/gen/dat/voc),
// singular/dual/plural, polytonic Greek script (Unicode), complex verb system with aspect
// (imperfective/perfective), augment in past tenses, pro-drop.
fn lang_profile_grc() -> [String] {
return lang_profile("grc", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case;aspect", "true")
}
// Old English (Anglo-Saxon): SOV/V2, fusional, 3 genders, 4 cases (nom/acc/gen/dat),
// singular/plural, Latin alphabet + þ/ð/ƿ/æ, strong and weak noun/verb classes, pro-drop.
fn lang_profile_ang() -> [String] {
return lang_profile("ang", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Old French (ca. 10001300 CE): SVO/V2, fusional, two-case system (nominative/oblique),
// two genders (masculine/feminine), left-to-right, agreement on number, person, gender,
// and case, no pro-drop (subject generally required).
fn lang_profile_fro() -> [String] {
return lang_profile("fro", "SVO", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Old High German (ca. 7501050 CE): SOV/V2, fusional, four-case system, three genders,
// left-to-right, agreement on number, person, gender, and case, pro-drop.
fn lang_profile_goh() -> [String] {
return lang_profile("goh", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Old Irish (ca. 600900 CE): VSO, fusional, case system, three genders,
// left-to-right, agreement on number, person, gender, and case, pro-drop.
fn lang_profile_sga() -> [String] {
return lang_profile("sga", "VSO", "fusional", "true", "true", "ltr", "number;person;gender;case", "true")
}
// Tocharian B (ca. 5001000 CE): SOV, fusional, case system, two genders,
// left-to-right, agreement on number, person, gender, and case, no pro-drop.
fn lang_profile_txb() -> [String] {
return lang_profile("txb", "SOV", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Old Persian (ca. 525330 BCE): SOV, fusional, 8-case system, no grammatical gender,
// left-to-right, agreement on number, person, and case, pro-drop.
fn lang_profile_peo() -> [String] {
return lang_profile("peo", "SOV", "fusional", "true", "false", "ltr", "number;person;case", "true")
}
// Akkadian (Old Babylonian period, ca. 19001600 BCE): VSO, fusional, 3-case system
// (nominative/accusative/genitive with mimation), two genders, left-to-right,
// agreement on number, person, gender, and case, no pro-drop.
fn lang_profile_akk() -> [String] {
return lang_profile("akk", "VSO", "fusional", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Ugaritic (ca. 14001200 BCE): VSO, Semitic trilateral root morphology, 3-case system,
// two genders, left-to-right (cuneiform alphabetic script), agreement on number, person,
// gender, and case, no pro-drop.
fn lang_profile_uga() -> [String] {
return lang_profile("uga", "VSO", "semitic", "true", "true", "ltr", "number;person;gender;case", "false")
}
// Ancient Egyptian / Middle Egyptian (ca. 21001300 BCE): SVO, agglutinative,
// no morphological case (word order + prepositions), two genders, left-to-right,
// agreement on number, person, and gender, pro-drop (zero copula in present).
fn lang_profile_egy() -> [String] {
return lang_profile("egy", "SVO", "agglutinative", "false", "true", "ltr", "number;person;gender", "true")
}
// Sumerian (ca. 30002000 BCE): SOV, agglutinative, ergative-absolutive case system,
// no grammatical gender (animacy distinction instead), left-to-right, agreement on
// number and person, pro-drop.
fn lang_profile_sux() -> [String] {
return lang_profile("sux", "SOV", "agglutinative", "true", "false", "ltr", "number;person", "true")
}
// Ge'ez (Classical Ethiopic, ca. 4th7th century CE): SOV, Semitic trilateral root
// morphology, two genders (masc/fem), Ethiopic/Fidel script (ltr), agreement on
// number, person, and gender, pro-drop (subject inflection on verb).
fn lang_profile_gez() -> [String] {
return lang_profile("gez", "SOV", "semitic", "true", "true", "ltr", "number;person;gender", "true")
}
// Coptic (Sahidic dialect, ca. 3rd11th century CE): SVO, agglutinative, no
// morphological case, two genders (masc/fem), left-to-right (Coptic alphabet),
// agreement on number and gender via bound subject pronouns, no pro-drop (explicit
// subject prefix required on every verb).
fn lang_profile_cop() -> [String] {
return lang_profile("cop", "SVO", "agglutinative", "false", "true", "ltr", "number;person;gender", "false")
}
// Dispatch: code -> profile
fn lang_from_code(code: String) -> [String] {
if str_eq(code, "en") { return lang_profile_en() }
if str_eq(code, "ja") { return lang_profile_ja() }
if str_eq(code, "ar") { return lang_profile_ar() }
if str_eq(code, "zh") { return lang_profile_zh() }
if str_eq(code, "de") { return lang_profile_de() }
if str_eq(code, "es") { return lang_profile_es() }
if str_eq(code, "fi") { return lang_profile_fi() }
if str_eq(code, "sw") { return lang_profile_sw() }
if str_eq(code, "hi") { return lang_profile_hi() }
if str_eq(code, "ru") { return lang_profile_ru() }
if str_eq(code, "fr") { return lang_profile_fr() }
if str_eq(code, "la") { return lang_profile_la() }
if str_eq(code, "he") { return lang_profile_he() }
if str_eq(code, "grc") { return lang_profile_grc() }
if str_eq(code, "ang") { return lang_profile_ang() }
if str_eq(code, "sa") { return lang_profile_sa() }
if str_eq(code, "got") { return lang_profile_got() }
if str_eq(code, "non") { return lang_profile_non() }
if str_eq(code, "enm") { return lang_profile_enm() }
if str_eq(code, "pi") { return lang_profile_pi() }
if str_eq(code, "fro") { return lang_profile_fro() }
if str_eq(code, "goh") { return lang_profile_goh() }
if str_eq(code, "sga") { return lang_profile_sga() }
if str_eq(code, "txb") { return lang_profile_txb() }
if str_eq(code, "peo") { return lang_profile_peo() }
if str_eq(code, "akk") { return lang_profile_akk() }
if str_eq(code, "uga") { return lang_profile_uga() }
if str_eq(code, "egy") { return lang_profile_egy() }
if str_eq(code, "sux") { return lang_profile_sux() }
if str_eq(code, "gez") { return lang_profile_gez() }
if str_eq(code, "cop") { return lang_profile_cop() }
// Unknown code: fall back to English profile
return lang_profile_en()
}
// English default - backward compatibility entry point.
fn lang_default() -> [String] {
return lang_profile_en()
}
// Typed convenience predicates
fn lang_is_isolating(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "morph_type"), "isolating")
}
fn lang_is_agglutinative(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "morph_type"), "agglutinative")
}
fn lang_is_fusional(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "morph_type"), "fusional")
}
fn lang_is_polysynthetic(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "morph_type"), "polysynthetic")
}
fn lang_is_rtl(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "script_dir"), "rtl")
}
fn lang_has_null_subject(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "null_subject"), "true")
}
fn lang_has_case(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "has_case"), "true")
}
fn lang_has_gender(profile: [String]) -> Bool {
return str_eq(lang_get(profile, "has_gender"), "true")
}
fn lang_word_order(profile: [String]) -> String {
return lang_get(profile, "word_order")
}
fn lang_code(profile: [String]) -> String {
return lang_get(profile, "code")
}
-46
View File
@@ -1,46 +0,0 @@
// auto-generated by elc --emit-header — do not edit
extern fn lang_profile(code: String, word_order: String, morph_type: String, has_case: String, has_gender: String, script_dir: String, agreement: String, null_subject: String) -> [String]
extern fn lang_get(profile: [String], key: String) -> String
extern fn lang_profile_en() -> [String]
extern fn lang_profile_ja() -> [String]
extern fn lang_profile_ar() -> [String]
extern fn lang_profile_zh() -> [String]
extern fn lang_profile_de() -> [String]
extern fn lang_profile_es() -> [String]
extern fn lang_profile_fi() -> [String]
extern fn lang_profile_sw() -> [String]
extern fn lang_profile_hi() -> [String]
extern fn lang_profile_ru() -> [String]
extern fn lang_profile_fr() -> [String]
extern fn lang_profile_la() -> [String]
extern fn lang_profile_he() -> [String]
extern fn lang_profile_sa() -> [String]
extern fn lang_profile_got() -> [String]
extern fn lang_profile_non() -> [String]
extern fn lang_profile_enm() -> [String]
extern fn lang_profile_pi() -> [String]
extern fn lang_profile_grc() -> [String]
extern fn lang_profile_ang() -> [String]
extern fn lang_profile_fro() -> [String]
extern fn lang_profile_goh() -> [String]
extern fn lang_profile_sga() -> [String]
extern fn lang_profile_txb() -> [String]
extern fn lang_profile_peo() -> [String]
extern fn lang_profile_akk() -> [String]
extern fn lang_profile_uga() -> [String]
extern fn lang_profile_egy() -> [String]
extern fn lang_profile_sux() -> [String]
extern fn lang_profile_gez() -> [String]
extern fn lang_profile_cop() -> [String]
extern fn lang_from_code(code: String) -> [String]
extern fn lang_default() -> [String]
extern fn lang_is_isolating(profile: [String]) -> Bool
extern fn lang_is_agglutinative(profile: [String]) -> Bool
extern fn lang_is_fusional(profile: [String]) -> Bool
extern fn lang_is_polysynthetic(profile: [String]) -> Bool
extern fn lang_is_rtl(profile: [String]) -> Bool
extern fn lang_has_null_subject(profile: [String]) -> Bool
extern fn lang_has_case(profile: [String]) -> Bool
extern fn lang_has_gender(profile: [String]) -> Bool
extern fn lang_word_order(profile: [String]) -> String
extern fn lang_code(profile: [String]) -> String
-40
View File
@@ -1,40 +0,0 @@
import "language-profile.el"
extern fn es_pluralize(noun: String) -> String
extern fn es_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fr_pluralize(noun: String) -> String
extern fn fr_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn de_noun_plural(noun: String, gender: String) -> String
extern fn de_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn ru_noun_case(noun: String, gender: String, gram_case: String, number: String) -> String
extern fn ru_conjugate(verb: String, tense: String, person: String, number: String, gender: String) -> String
extern fn ja_conjugate(dict_form: String, form: String) -> String
extern fn fi_apply_case(noun: String, gram_case: String, number: String) -> String
extern fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn ar_sound_plural(noun: String, gender: String) -> String
extern fn ar_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
extern fn hi_noun_direct(noun: String, gender: String, number: String) -> String
extern fn hi_gender(noun: String) -> String
extern fn hi_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
extern fn sw_noun_plural(noun: String) -> String
extern fn sw_conjugate(verb: String, person: String, number: String, noun_class: String, tense: String) -> String
extern fn la_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn he_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String
extern fn grc_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn ang_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn sa_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn got_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn non_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn enm_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn pi_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn fro_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn goh_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn sga_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn txb_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn peo_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn uga_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn egy_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn sux_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn gez_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn cop_conjugate(verb: String, tense: String, person: String, number: String) -> String
-3
View File
@@ -1,3 +0,0 @@
fn morph_tiny(x: String) -> String {
return x
}
-528
View File
@@ -1,528 +0,0 @@
// morphology-akk.el - Akkadian morphology for the NLG engine.
// 𒀭𒂗𒍪 Akkadian (akkadû), the language of Babylon and Assyria.
//
// Implements Old Babylonian Akkadian verb conjugation (G-stem / Grundstamm),
// noun declension with mimation, and noun-phrase construction.
//
// Akkadian is the oldest attested Semitic language (ca. 2800100 BCE).
// It uses cuneiform script; we work in standard Latin transliteration
// (Old Babylonian dialect the classical prestige form).
//
// Language profile:
// code=akk, name=Akkadian, morph_type=semitic, word_order=VSO/SOV,
// script=cuneiform (transliterated), family=semitic/east-semitic
//
// Key grammatical facts:
// - Semitic trilateral root system: words built from 3-consonant roots
// by inserting vowel patterns (e.g. root p-r-s iparras "he decides")
// - Grammatical gender: masculine / feminine (no neuter)
// - Cases: nominative (-um), accusative (-am), genitive (-im) "mimation"
// - Number: singular / plural (dual is vestigial in verbs)
// - Verb stems: G (basic), D (intensive), Š (causative), N (passive);
// this file implements G-stem throughout
// - Two main tense/aspect systems:
// Present-future (iparras pattern): action in progress or future
// Perfect (iptaras pattern): completed action with present relevance
// Stative (paris pattern): resultant state, often adjectival
// - No definite or indefinite article; case endings convey
// determination contextually
// - Copula: bašû (to exist/be)
//
// Verb conjugation conventions:
// person: "first" | "second" | "third"
// gender: "m" | "f"
// number: "singular" | "plural"
// tense: "present" | "perfect" | "stative"
//
// Noun declension conventions:
// gram_case: "nom" | "acc" | "gen"
// number: "singular" | "plural"
// gender: "m" | "f" (passed to akk_decline for gender-specific forms)
//
// Verbs covered (G-stem infinitive, transliterated):
// "bašû" to exist / be (copula)
// "alāku" to go
// "amāru" to see
// "qabû" to say
// "epēšu" to do / make
//
// Nouns covered with known mimation forms:
// "šarrum" king
// "awīlum" man / person
// "bītum" house
// "ilum" god
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn akk_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn akk_str_len(s: String) -> Int {
return str_len(s)
}
fn akk_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
// Slot index
//
// Maps person × number to a 0-based slot for table lookups.
// Akkadian verb agreement does not distinguish gender in 1st person,
// and the 2nd person often conflates masc/fem in some paradigms.
// We use a 6-cell paradigm matching the most common OB presentation:
//
// 0 = 1sg (I)
// 1 = 2sg (you sg)
// 2 = 3sg m (he)
// 3 = 3sg f (she)
// 4 = 1pl (we)
// 5 = 3pl (they)
//
// Note: 2pl is rare / vestigial in attested OB texts; omitted here.
fn akk_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "plural") { return 4 }
return 0
}
if str_eq(person, "second") {
return 1
}
// third
if str_eq(number, "plural") { return 5 }
return 2 // default: 3sg masc; caller may override with gender check below
}
// akk_slot_g: gender-aware slot for third person singular.
// Returns 3 (3sg fem) when person=third, number=singular, gender=f.
fn akk_slot_g(person: String, gender: String, number: String) -> Int {
let base: Int = akk_slot(person, number)
if str_eq(person, "third") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 3 }
}
}
return base
}
// Copula: bašû to exist / be
//
// bašû is suppletive and highly irregular.
// Present: ibašši (3sg m/f), abašši (1sg), tabašši (2sg)
// Stative: bašī (3sg m), bašiat (3sg f), bašāku (1sg)
// Perfect: not commonly attested in G-stem; use present forms as fallback.
fn akk_copula_present(slot: Int) -> String {
if slot == 0 { return "abašši" } // 1sg
if slot == 1 { return "tabašši" } // 2sg
if slot == 2 { return "ibašši" } // 3sg m
if slot == 3 { return "ibašši" } // 3sg f (same form in attested OB)
if slot == 4 { return "nibašši" } // 1pl
return "ibaššū" // 3pl
}
fn akk_copula_stative(slot: Int) -> String {
if slot == 0 { return "bašāku" } // 1sg (stative 1sg: -āku suffix)
if slot == 1 { return "bašāta" } // 2sg (-āta suffix)
if slot == 2 { return "bašī" } // 3sg m (unmarked base)
if slot == 3 { return "bašiat" } // 3sg f (-at suffix)
if slot == 4 { return "bašānu" } // 1pl (-ānu suffix)
return "bašū" // 3pl ( suffix)
}
fn akk_is_copula(verb: String) -> Bool {
if str_eq(verb, "bašû") { return true }
if str_eq(verb, "bashu") { return true }
if str_eq(verb, "be") { return true }
return false
}
fn akk_conjugate_copula(tense: String, slot: Int) -> String {
if str_eq(tense, "stative") { return akk_copula_stative(slot) }
// present and perfect both fall back to present forms for bašû
return akk_copula_present(slot)
}
// alāku to go
//
// Irregular: present stem is illak- (not the expected alakk-).
// Present: illak (3sg), allak (1sg), tallak (2sg), nillak (1pl), illaku (3pl)
// Perfect: ittalk- forms (less common, use illak- + perf marker)
// Stative: use present as proxy
fn akk_alaku_present(slot: Int) -> String {
if slot == 0 { return "allak" } // 1sg
if slot == 1 { return "tallak" } // 2sg
if slot == 2 { return "illak" } // 3sg m
if slot == 3 { return "tallak" } // 3sg f (same as 2sg OB pattern)
if slot == 4 { return "nillak" } // 1pl
return "illaku" // 3pl
}
fn akk_alaku_perfect(slot: Int) -> String {
if slot == 0 { return "ittalak" } // 1sg
if slot == 1 { return "tattalak" } // 2sg
if slot == 2 { return "ittalak" } // 3sg m
if slot == 3 { return "tattalak" } // 3sg f
if slot == 4 { return "nittalak" } // 1pl
return "ittalku" // 3pl
}
// amāru to see
//
// Present (immar-): immar (3sg), ammar (1sg), tammar (2sg)
// Perfect (imtamar-): imtamar (3sg), amtamar (1sg), tamtamar (2sg)
fn akk_amaru_present(slot: Int) -> String {
if slot == 0 { return "ammar" } // 1sg
if slot == 1 { return "tammar" } // 2sg
if slot == 2 { return "immar" } // 3sg m
if slot == 3 { return "tammar" } // 3sg f
if slot == 4 { return "nimmar" } // 1pl
return "immaru" // 3pl
}
fn akk_amaru_perfect(slot: Int) -> String {
if slot == 0 { return "amtamar" } // 1sg
if slot == 1 { return "tamtamar" } // 2sg
if slot == 2 { return "imtamar" } // 3sg m
if slot == 3 { return "tamtamar" } // 3sg f
if slot == 4 { return "nimtamar" } // 1pl
return "imtamaru" // 3pl
}
fn akk_amaru_stative(slot: Int) -> String {
// amāru stative: 3sg "amir" (the one who saw / he has seen)
if slot == 0 { return "amrāku" }
if slot == 1 { return "amrāta" }
if slot == 2 { return "amir" }
if slot == 3 { return "amrat" }
if slot == 4 { return "amrānu" }
return "amrū"
}
// qabû to say / speak
//
// Present: iqabbi (3sg), aqabbi (1sg), taqabbi (2sg)
// Perfect: iqtabi (3sg), aqtabi (1sg), taqtabi (2sg)
fn akk_qabu_present(slot: Int) -> String {
if slot == 0 { return "aqabbi" } // 1sg
if slot == 1 { return "taqabbi" } // 2sg
if slot == 2 { return "iqabbi" } // 3sg m
if slot == 3 { return "taqabbi" } // 3sg f
if slot == 4 { return "niqabbi" } // 1pl
return "iqabbû" // 3pl
}
fn akk_qabu_perfect(slot: Int) -> String {
if slot == 0 { return "aqtabi" } // 1sg
if slot == 1 { return "taqtabi" } // 2sg
if slot == 2 { return "iqtabi" } // 3sg m
if slot == 3 { return "taqtabi" } // 3sg f
if slot == 4 { return "niqtabi" } // 1pl
return "iqtabû" // 3pl
}
fn akk_qabu_stative(slot: Int) -> String {
if slot == 0 { return "qabāku" }
if slot == 1 { return "qabāta" }
if slot == 2 { return "qabi" }
if slot == 3 { return "qabiat" }
if slot == 4 { return "qabānu" }
return "qabû"
}
// epēšu to do / make
//
// Present (ieppuš / eppuš): ieppuš (3sg), eppuš (1sg), teppuš (2sg)
// Perfect: iptešu forms
fn akk_epesu_present(slot: Int) -> String {
if slot == 0 { return "eppuš" } // 1sg
if slot == 1 { return "teppuš" } // 2sg
if slot == 2 { return "ieppuš" } // 3sg m
if slot == 3 { return "teppuš" } // 3sg f
if slot == 4 { return "neppuš" } // 1pl
return "ieppušu" // 3pl
}
fn akk_epesu_perfect(slot: Int) -> String {
if slot == 0 { return "iptešu" } // 1sg (irregular: root ʿ-p-š)
if slot == 1 { return "taptešu" } // 2sg
if slot == 2 { return "iptešu" } // 3sg m
if slot == 3 { return "taptešu" } // 3sg f
if slot == 4 { return "niptešu" } // 1pl
return "iptešū" // 3pl
}
fn akk_epesu_stative(slot: Int) -> String {
if slot == 0 { return "epšāku" }
if slot == 1 { return "epšāta" }
if slot == 2 { return "epuš" }
if slot == 3 { return "epšat" }
if slot == 4 { return "epšānu" }
return "epšū"
}
// Regular G-stem paradigms (iparras model)
//
// For regular verbs not in the irregular table, we apply the standard
// OB G-stem paradigm using a caller-supplied present stem and perfect stem.
// The stems must be pre-computed by the caller (or vocabulary layer).
//
// iparras (present) endings by slot:
// 1sg: a- prefix
// 2sg: ta- prefix
// 3sg m: i- prefix
// 3sg f: ta- prefix (same prefix as 2sg)
// 1pl: ni- prefix
// 3pl: i- prefix + suffix
//
// For the generic fallback we use "iparras" as the model template.
fn akk_regular_present(stem: String, slot: Int) -> String {
// stem is the 3sg m form (i-prefix already present in conventional citation)
// We rebuild from the bare root portion by stripping/adding prefixes.
// Simplification: return prefixed forms using the provided present-3sg string.
if slot == 0 { return "a" + stem } // 1sg: a + stem (strip i-, add a-)
if slot == 1 { return "ta" + stem } // 2sg
if slot == 2 { return "i" + stem } // 3sg m
if slot == 3 { return "ta" + stem } // 3sg f
if slot == 4 { return "ni" + stem } // 1pl
return "i" + stem + "u" // 3pl: i + stem +
}
fn akk_regular_perfect(stem: String, slot: Int) -> String {
// Perfect (iptaras) uses infix -ta- after first root consonant.
// stem here is the 3sg perfect form; we apply person endings.
if slot == 0 { return "a" + stem } // 1sg
if slot == 1 { return "ta" + stem } // 2sg
if slot == 2 { return "i" + stem } // 3sg m
if slot == 3 { return "ta" + stem } // 3sg f
if slot == 4 { return "ni" + stem } // 1pl
return "i" + stem + "u" // 3pl
}
fn akk_regular_stative(stem: String, slot: Int) -> String {
// Stative (paris): 3sg m has zero ending; others take person suffixes.
if slot == 0 { return stem + "āku" } // 1sg
if slot == 1 { return stem + "āta" } // 2sg
if slot == 2 { return stem } // 3sg m: bare stem
if slot == 3 { return stem + "at" } // 3sg f
if slot == 4 { return stem + "ānu" } // 1pl
return stem + "ū" // 3pl
}
// Known-verb dispatcher
fn akk_known_verb(verb: String, tense: String, slot: Int) -> String {
// bašû to be / exist
if str_eq(verb, "bašû") {
return akk_conjugate_copula(tense, slot)
}
if str_eq(verb, "bashu") {
return akk_conjugate_copula(tense, slot)
}
// alāku to go
if str_eq(verb, "alāku") {
if str_eq(tense, "perfect") { return akk_alaku_perfect(slot) }
if str_eq(tense, "stative") { return akk_alaku_present(slot) }
return akk_alaku_present(slot)
}
if str_eq(verb, "alaku") {
if str_eq(tense, "perfect") { return akk_alaku_perfect(slot) }
return akk_alaku_present(slot)
}
// amāru to see
if str_eq(verb, "amāru") {
if str_eq(tense, "perfect") { return akk_amaru_perfect(slot) }
if str_eq(tense, "stative") { return akk_amaru_stative(slot) }
return akk_amaru_present(slot)
}
if str_eq(verb, "amaru") {
if str_eq(tense, "perfect") { return akk_amaru_perfect(slot) }
if str_eq(tense, "stative") { return akk_amaru_stative(slot) }
return akk_amaru_present(slot)
}
// qabû to say
if str_eq(verb, "qabû") {
if str_eq(tense, "perfect") { return akk_qabu_perfect(slot) }
if str_eq(tense, "stative") { return akk_qabu_stative(slot) }
return akk_qabu_present(slot)
}
if str_eq(verb, "qabu") {
if str_eq(tense, "perfect") { return akk_qabu_perfect(slot) }
if str_eq(tense, "stative") { return akk_qabu_stative(slot) }
return akk_qabu_present(slot)
}
// epēšu to do / make
if str_eq(verb, "epēšu") {
if str_eq(tense, "perfect") { return akk_epesu_perfect(slot) }
if str_eq(tense, "stative") { return akk_epesu_stative(slot) }
return akk_epesu_present(slot)
}
if str_eq(verb, "epesu") {
if str_eq(tense, "perfect") { return akk_epesu_perfect(slot) }
if str_eq(tense, "stative") { return akk_epesu_stative(slot) }
return akk_epesu_present(slot)
}
return ""
}
// Main conjugation entry point
//
// akk_conjugate: conjugate an Akkadian verb (G-stem).
//
// verb: G-stem infinitive (transliterated, e.g. "alāku", "amāru")
// tense: "present" | "perfect" | "stative"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns:
// - Inflected form for known verbs
// - verb unchanged as safe fallback for unknown verbs
fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let slot: Int = akk_slot(person, number)
// Copula shortcut
if akk_is_copula(verb) {
return akk_conjugate_copula(tense, slot)
}
// Known-verb table
let known: String = akk_known_verb(verb, tense, slot)
if !str_eq(known, "") {
return known
}
// Unknown verb: safe fallback
return verb
}
// Noun declension
//
// akk_decline: decline an Akkadian noun for gram_case and number.
//
// Mimation: OB nouns bear final -m in all case endings (mimation).
// The base noun (dictionary form) is the nominative singular with mimation.
// We strip the nominative -um ending (if present) to obtain the bare stem,
// then apply the requested ending.
//
// Masculine case endings (singular):
// Nominative: -um
// Accusative: -am
// Genitive: -im
//
// Masculine case endings (plural):
// Nominative: -ūtum (or in construct)
// Accusative/Genitive: -ātim (or in construct)
//
// Feminine nouns (identified by -tum nom sg ending):
// Sg nominative: -tum, accusative: -tam, genitive: -tim
// Pl nominative: -ātum, genitive/accusative: -ātim
//
// Known irregular stems (the vocabulary layer should pass dictionary forms):
// šarrum stem: šarr-
// awīlum stem: awīl-
// bītum stem: bīt-
// ilum stem: il-
fn akk_strip_nom(noun: String) -> String {
// Strip -um (masc nom sg mimation ending) to get bare stem
if akk_str_ends(noun, "um") {
return akk_str_drop_last(noun, 2)
}
// Strip -tum (fem nom sg)
if akk_str_ends(noun, "tum") {
return akk_str_drop_last(noun, 3)
}
// Already a bare stem or unusual form: return as-is
return noun
}
fn akk_is_fem(noun: String) -> Bool {
// Feminine nouns in OB typically end in -tum (nom sg)
if akk_str_ends(noun, "tum") { return true }
if akk_str_ends(noun, "tam") { return true }
if akk_str_ends(noun, "tim") { return true }
return false
}
fn akk_decline(noun: String, gram_case: String, number: String) -> String {
let fem: Bool = akk_is_fem(noun)
let stem: String = akk_strip_nom(noun)
if str_eq(number, "singular") {
if fem {
if str_eq(gram_case, "nom") { return stem + "tum" }
if str_eq(gram_case, "acc") { return stem + "tam" }
if str_eq(gram_case, "gen") { return stem + "tim" }
return stem + "tum"
}
// Masculine
if str_eq(gram_case, "nom") { return stem + "um" }
if str_eq(gram_case, "acc") { return stem + "am" }
if str_eq(gram_case, "gen") { return stem + "im" }
return stem + "um"
}
// Plural
if fem {
if str_eq(gram_case, "nom") { return stem + "ātum" }
// acc and gen merge in the oblique plural
return stem + "ātim"
}
// Masculine plural
if str_eq(gram_case, "nom") { return stem + "ūtum" }
return stem + "ātim"
}
// Noun phrase
//
// akk_noun_phrase: produce the surface noun phrase.
//
// Akkadian has no definite or indefinite article. Determination is conveyed
// by context, word order, and the genitive construct chain (status constructus).
// The definite parameter is accepted but has no surface effect: the declined
// noun is returned in either case.
//
// noun: dictionary form (nominative singular with mimation, e.g. "šarrum")
// gram_case: "nom" | "acc" | "gen"
// number: "singular" | "plural"
// definite: "true" | "false" (no surface effect in Akkadian)
fn akk_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return akk_decline(noun, gram_case, number)
}
// Canonical verb mapping
//
// akk_map_canonical: map cross-lingual English canonical verb labels to
// their Akkadian G-stem infinitive equivalents.
fn akk_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "bašû" }
if str_eq(verb, "go") { return "alāku" }
if str_eq(verb, "see") { return "amāru" }
if str_eq(verb, "say") { return "qabû" }
if str_eq(verb, "speak") { return "qabû" }
if str_eq(verb, "do") { return "epēšu" }
if str_eq(verb, "make") { return "epēšu" }
return verb
}
-31
View File
@@ -1,31 +0,0 @@
// auto-generated by elc --emit-header — do not edit
extern fn akk_str_ends(s: String, suf: String) -> Bool
extern fn akk_str_len(s: String) -> Int
extern fn akk_str_drop_last(s: String, n: Int) -> String
extern fn akk_slot(person: String, number: String) -> Int
extern fn akk_slot_g(person: String, gender: String, number: String) -> Int
extern fn akk_copula_present(slot: Int) -> String
extern fn akk_copula_stative(slot: Int) -> String
extern fn akk_is_copula(verb: String) -> Bool
extern fn akk_conjugate_copula(tense: String, slot: Int) -> String
extern fn akk_alaku_present(slot: Int) -> String
extern fn akk_alaku_perfect(slot: Int) -> String
extern fn akk_amaru_present(slot: Int) -> String
extern fn akk_amaru_perfect(slot: Int) -> String
extern fn akk_amaru_stative(slot: Int) -> String
extern fn akk_qabu_present(slot: Int) -> String
extern fn akk_qabu_perfect(slot: Int) -> String
extern fn akk_qabu_stative(slot: Int) -> String
extern fn akk_epesu_present(slot: Int) -> String
extern fn akk_epesu_perfect(slot: Int) -> String
extern fn akk_epesu_stative(slot: Int) -> String
extern fn akk_regular_present(stem: String, slot: Int) -> String
extern fn akk_regular_perfect(stem: String, slot: Int) -> String
extern fn akk_regular_stative(stem: String, slot: Int) -> String
extern fn akk_known_verb(verb: String, tense: String, slot: Int) -> String
extern fn akk_conjugate(verb: String, tense: String, person: String, number: String) -> String
extern fn akk_strip_nom(noun: String) -> String
extern fn akk_is_fem(noun: String) -> Bool
extern fn akk_decline(noun: String, gram_case: String, number: String) -> String
extern fn akk_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String
extern fn akk_map_canonical(verb: String) -> String
-752
View File
@@ -1,752 +0,0 @@
// morphology-ang.el - Old English (Anglo-Saxon) morphology for the NLG engine.
//
// Implements Old English verb conjugation, noun declension, and the definite
// article/demonstrative pronoun. Designed as a companion to morphology.el and
// called by the engine when the language profile code is "ang".
//
// Language profile: code=ang, name=Old English, morph_type=fusional,
// word_order=SOV, question_strategy=intonation, script=latin, family=germanic.
//
// Typology note: Old English is a synthetic Germanic language with four
// grammatical cases (nominative, accusative, genitive, dative), three genders,
// and strong/weak noun and verb classes. Strong verbs form their past tense by
// internal vowel change (ablaut); weak verbs use a dental (-de/-ode) suffix.
// Long vowels are marked with a macron (ā ē ī ō ū) and are preserved in all
// string literals; ǣ, æ, þ, ð, and ƿ (wynn) are used where historically
// appropriate. V2 (verb-second) word order applies in main clauses but is not
// enforced by this module the realizer handles constituent ordering.
//
// Verb conjugation covered:
// Tenses: present, past
// Persons: first/second/third × singular/plural (slots 0-5)
// Classes: weak (regular -ian), strong irregular table
// Irregulars: wesan/beon (be), habban (have), gān (go), cuman (come),
// secgan (say), sēon (see), dōn (do), willan (want), magan (can)
// Canonical map: "be" -> "wesan" (past) / "beon" (present)
//
// Noun declension covered:
// Strong masc a-stem (cyning pattern): nom/acc -, gen -es, dat -e; pl -as/-a/-um
// Strong neut a-stem (word pattern): sg same as masc; pl nom/acc -∅
// Weak n-stem (nama pattern): sg nom -a, obl -an; pl -an/-ena/-um
//
// Article: simplified demonstrative/article forms for masculine, feminine,
// neuter (se/sēo/þæt), fully declined.
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
// String helpers
import "morphology.el"
fn ang_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn ang_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn ang_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn ang_str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
// Person/number slot
//
// Maps person × number to a 0-based index for paradigm tables.
// 0 = 1st singular (ic)
// 1 = 2nd singular (þū)
// 2 = 3rd singular (hē/hēo/hit)
// 3 = 1st plural ()
// 4 = 2nd plural ()
// 5 = 3rd plural (hīe)
//
// Old English also has a dual (wit, git) not handled; dual falls through
// to plural.
fn ang_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Canonical verb mapping
//
// The semantic layer may pass English canonical labels. Map to Old English
// citation (infinitive) forms. "be" maps to "beon" for present and "wesan"
// for past the caller selects tense, so we map "be" to "beon" and handle
// the past-tense wesan forms inside the conjugation function.
fn ang_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "beon" }
if str_eq(verb, "have") { return "habban" }
if str_eq(verb, "go") { return "gān" }
if str_eq(verb, "come") { return "cuman" }
if str_eq(verb, "say") { return "secgan" }
if str_eq(verb, "see") { return "sēon" }
if str_eq(verb, "do") { return "dōn" }
if str_eq(verb, "want") { return "willan" }
if str_eq(verb, "will") { return "willan" }
if str_eq(verb, "can") { return "magan" }
if str_eq(verb, "know") { return "witan" }
if str_eq(verb, "give") { return "giefan" }
if str_eq(verb, "take") { return "niman" }
if str_eq(verb, "find") { return "findan" }
if str_eq(verb, "make") { return "macian" }
return verb
}
// Irregular: wesan (to be past tense forms)
//
// Past: wæs wǣre wæs wǣron wǣron wǣron
fn ang_wesan_past(slot: Int) -> String {
if slot == 0 { return "wæs" }
if slot == 1 { return "wǣre" }
if slot == 2 { return "wæs" }
if slot == 3 { return "wǣron" }
if slot == 4 { return "wǣron" }
return "wǣron"
}
// Irregular: beon (to be present / habitual / future)
//
// Present: bēo bist biþ bēoþ bēoþ bēoþ
//
// The present indicative of "wesan" is eom/eart/is/sind that paradigm is
// also provided below for completeness and for callers who specifically request
// wesan present.
fn ang_beon_present(slot: Int) -> String {
if slot == 0 { return "bēo" }
if slot == 1 { return "bist" }
if slot == 2 { return "biþ" }
if slot == 3 { return "bēoþ" }
if slot == 4 { return "bēoþ" }
return "bēoþ"
}
// Irregular: wesan present (eom/eart/is/sind)
//
// Present: eom eart is sind/sindon sind sind
fn ang_wesan_present(slot: Int) -> String {
if slot == 0 { return "eom" }
if slot == 1 { return "eart" }
if slot == 2 { return "is" }
if slot == 3 { return "sind" }
if slot == 4 { return "sind" }
return "sind"
}
// Irregular: habban (to have)
//
// Present: hæbbe hæfst hæfþ habbað habbað habbað
// Past: hæfde hæfdest hæfde hæfdon hæfdon hæfdon
fn ang_habban_present(slot: Int) -> String {
if slot == 0 { return "hæbbe" }
if slot == 1 { return "hæfst" }
if slot == 2 { return "hæfþ" }
if slot == 3 { return "habbað" }
if slot == 4 { return "habbað" }
return "habbað"
}
fn ang_habban_past(slot: Int) -> String {
if slot == 0 { return "hæfde" }
if slot == 1 { return "hæfdest" }
if slot == 2 { return "hæfde" }
if slot == 3 { return "hæfdon" }
if slot == 4 { return "hæfdon" }
return "hæfdon"
}
// Irregular: gān (to go)
//
// Present: gǣst gǣþ gāð gāð gāð
// Past: ēode ēodest ēode ēodon ēodon ēodon
fn ang_gan_present(slot: Int) -> String {
if slot == 0 { return "" }
if slot == 1 { return "gǣst" }
if slot == 2 { return "gǣþ" }
if slot == 3 { return "gāð" }
if slot == 4 { return "gāð" }
return "gāð"
}
fn ang_gan_past(slot: Int) -> String {
if slot == 0 { return "ēode" }
if slot == 1 { return "ēodest" }
if slot == 2 { return "ēode" }
if slot == 3 { return "ēodon" }
if slot == 4 { return "ēodon" }
return "ēodon"
}
// Irregular: cuman (to come)
//
// Present: cume cymst cymþ cumað cumað cumað
// Past: cōm cōme cōm cōmon cōmon cōmon
fn ang_cuman_present(slot: Int) -> String {
if slot == 0 { return "cume" }
if slot == 1 { return "cymst" }
if slot == 2 { return "cymþ" }
if slot == 3 { return "cumað" }
if slot == 4 { return "cumað" }
return "cumað"
}
fn ang_cuman_past(slot: Int) -> String {
if slot == 0 { return "cōm" }
if slot == 1 { return "cōme" }
if slot == 2 { return "cōm" }
if slot == 3 { return "cōmon" }
if slot == 4 { return "cōmon" }
return "cōmon"
}
// Irregular: secgan (to say)
//
// Present: secge sagast sagað secgað secgað secgað
// Past: sægde sægdest sægde sægdon sægdon sægdon
fn ang_secgan_present(slot: Int) -> String {
if slot == 0 { return "secge" }
if slot == 1 { return "sagast" }
if slot == 2 { return "sagað" }
if slot == 3 { return "secgað" }
if slot == 4 { return "secgað" }
return "secgað"
}
fn ang_secgan_past(slot: Int) -> String {
if slot == 0 { return "sægde" }
if slot == 1 { return "sægdest" }
if slot == 2 { return "sægde" }
if slot == 3 { return "sægdon" }
if slot == 4 { return "sægdon" }
return "sægdon"
}
// Irregular: sēon (to see)
//
// Present: sēo siehst siehþ sēoð sēoð sēoð
// Past: seah sāwe seah sāwon sāwon sāwon
fn ang_seon_present(slot: Int) -> String {
if slot == 0 { return "sēo" }
if slot == 1 { return "siehst" }
if slot == 2 { return "siehþ" }
if slot == 3 { return "sēoð" }
if slot == 4 { return "sēoð" }
return "sēoð"
}
fn ang_seon_past(slot: Int) -> String {
if slot == 0 { return "seah" }
if slot == 1 { return "sāwe" }
if slot == 2 { return "seah" }
if slot == 3 { return "sāwon" }
if slot == 4 { return "sāwon" }
return "sāwon"
}
// Irregular: dōn (to do)
//
// Present: dēst dēþ dōð dōð dōð
// Past: dyde dydest dyde dydon dydon dydon
fn ang_don_present(slot: Int) -> String {
if slot == 0 { return "" }
if slot == 1 { return "dēst" }
if slot == 2 { return "dēþ" }
if slot == 3 { return "dōð" }
if slot == 4 { return "dōð" }
return "dōð"
}
fn ang_don_past(slot: Int) -> String {
if slot == 0 { return "dyde" }
if slot == 1 { return "dydest" }
if slot == 2 { return "dyde" }
if slot == 3 { return "dydon" }
if slot == 4 { return "dydon" }
return "dydon"
}
// Irregular: willan (to want / will)
//
// Present: wille wilt wile willað willað willað
// Past: wolde woldest wolde woldon woldon woldon
fn ang_willan_present(slot: Int) -> String {
if slot == 0 { return "wille" }
if slot == 1 { return "wilt" }
if slot == 2 { return "wile" }
if slot == 3 { return "willað" }
if slot == 4 { return "willað" }
return "willað"
}
fn ang_willan_past(slot: Int) -> String {
if slot == 0 { return "wolde" }
if slot == 1 { return "woldest" }
if slot == 2 { return "wolde" }
if slot == 3 { return "woldon" }
if slot == 4 { return "woldon" }
return "woldon"
}
// Irregular: magan (to be able / can)
//
// Present: mæg meaht mæg magon magon magon
// Past: meahte meahtest meahte meahton meahton meahton
fn ang_magan_present(slot: Int) -> String {
if slot == 0 { return "mæg" }
if slot == 1 { return "meaht" }
if slot == 2 { return "mæg" }
if slot == 3 { return "magon" }
if slot == 4 { return "magon" }
return "magon"
}
fn ang_magan_past(slot: Int) -> String {
if slot == 0 { return "meahte" }
if slot == 1 { return "meahtest" }
if slot == 2 { return "meahte" }
if slot == 3 { return "meahton" }
if slot == 4 { return "meahton" }
return "meahton"
}
// Irregular: witan (to know)
//
// Present: wāt wāst wāt witon witon witon
// Past: wisse/wiste wissest wisse wisson wisson wisson
fn ang_witan_present(slot: Int) -> String {
if slot == 0 { return "wāt" }
if slot == 1 { return "wāst" }
if slot == 2 { return "wāt" }
if slot == 3 { return "witon" }
if slot == 4 { return "witon" }
return "witon"
}
fn ang_witan_past(slot: Int) -> String {
if slot == 0 { return "wisse" }
if slot == 1 { return "wissest" }
if slot == 2 { return "wisse" }
if slot == 3 { return "wisson" }
if slot == 4 { return "wisson" }
return "wisson"
}
// Weak verb: present-tense endings
//
// Weak verbs with -ian infinitives form their present tense as:
// stem + -e, -est, -eþ, -aþ, -aþ, -aþ
//
// The stem is the infinitive with -ian stripped (or -an for class-2 verbs).
fn ang_weak_present_ending(slot: Int) -> String {
if slot == 0 { return "e" }
if slot == 1 { return "est" }
if slot == 2 { return "" }
if slot == 3 { return "" }
if slot == 4 { return "" }
return ""
}
// Weak verb: past-tense ending selection
//
// Class 1 (-ian with short stem): past -ede (e.g. nerian -> nerede)
// Class 2 (-ian with long/heavy stem): past -ode (e.g. macian -> macode)
// Class 3 (-ian, small group): past -de (e.g. habban -> hæfde irregular)
//
// Heuristic: if the stem length is 1 char, use -ede; otherwise use -ode.
// This is a simplification; correct assignment requires lexical class marking.
//
// For the past, all persons in the plural share -on, and all singulars share
// the same dental-suffixed stem.
fn ang_weak_past_stem(stem: String) -> String {
let slen: Int = str_len(stem)
if slen <= 2 {
return stem + "ede"
}
return stem + "ode"
}
fn ang_weak_past(stem: String, slot: Int) -> String {
let pstem: String = ang_weak_past_stem(stem)
if slot == 0 { return pstem }
if slot == 1 { return pstem + "st" }
if slot == 2 { return pstem }
if slot == 3 { return ang_str_drop_last(pstem, 1) + "on" }
if slot == 4 { return ang_str_drop_last(pstem, 1) + "on" }
return ang_str_drop_last(pstem, 1) + "on"
}
// Stem extraction for weak verbs
//
// Strip the infinitive ending to recover the stem:
// -ian -> strip 3 chars (nerian -> ner-, macian -> mac-)
// -an -> strip 2 chars (habban -> habb-; fallback for non -ian)
// otherwise: return as-is
fn ang_weak_stem(verb: String) -> String {
if ang_str_ends(verb, "ian") {
return ang_str_drop_last(verb, 3)
}
if ang_str_ends(verb, "an") {
return ang_str_drop_last(verb, 2)
}
return verb
}
// ang_conjugate: main conjugation entry point
//
// verb: Old English infinitive or English canonical label
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Strategy:
// 1. Map canonical English labels to OE verbs.
// 2. Check the full irregular table.
// 3. Fall back to weak conjugation for unknown -ian/-an verbs.
// 4. Return the base form if nothing matches.
fn ang_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = ang_map_canonical(verb)
let slot: Int = ang_slot(person, number)
// Irregulars
// beon: present-tense "be" (habitual/future/general)
if str_eq(v, "beon") {
if str_eq(tense, "present") { return ang_beon_present(slot) }
// past: use wesan past forms
return ang_wesan_past(slot)
}
// wesan: past "be" and present "be" (existential/stative)
if str_eq(v, "wesan") {
if str_eq(tense, "present") { return ang_wesan_present(slot) }
return ang_wesan_past(slot)
}
if str_eq(v, "habban") {
if str_eq(tense, "present") { return ang_habban_present(slot) }
return ang_habban_past(slot)
}
if str_eq(v, "gān") {
if str_eq(tense, "present") { return ang_gan_present(slot) }
return ang_gan_past(slot)
}
if str_eq(v, "cuman") {
if str_eq(tense, "present") { return ang_cuman_present(slot) }
return ang_cuman_past(slot)
}
if str_eq(v, "secgan") {
if str_eq(tense, "present") { return ang_secgan_present(slot) }
return ang_secgan_past(slot)
}
if str_eq(v, "sēon") {
if str_eq(tense, "present") { return ang_seon_present(slot) }
return ang_seon_past(slot)
}
if str_eq(v, "dōn") {
if str_eq(tense, "present") { return ang_don_present(slot) }
return ang_don_past(slot)
}
if str_eq(v, "willan") {
if str_eq(tense, "present") { return ang_willan_present(slot) }
return ang_willan_past(slot)
}
if str_eq(v, "magan") {
if str_eq(tense, "present") { return ang_magan_present(slot) }
return ang_magan_past(slot)
}
if str_eq(v, "witan") {
if str_eq(tense, "present") { return ang_witan_present(slot) }
return ang_witan_past(slot)
}
// Regular weak conjugation
let stem: String = ang_weak_stem(v)
if str_eq(tense, "present") {
return stem + ang_weak_present_ending(slot)
}
if str_eq(tense, "past") {
return ang_weak_past(stem, slot)
}
// Unknown tense: return infinitive
return v
}
// Noun declension class detection
//
// Infer the declension class from the nominative singular form and an optional
// gender hint. Without a full lexicon, ending-based heuristics are used:
//
// ends in -a -> weak n-stem (nama pattern)
// ends in -e (long) -> may be various; default to strong masc a-stem
// any other ending -> strong a-stem; gender distinguishes masc vs neut
//
// The caller may pass gender as a hint:
// "masculine" | "feminine" | "neuter" | "" (empty = infer)
//
// For simplicity this module handles three paradigms:
// "strong_masc" a-stem masculine (cyning, mann)
// "strong_neut" a-stem neuter (word, scip)
// "weak" n-stem (nama, ēage)
fn ang_declension(noun: String, gender: String) -> String {
if ang_str_ends(noun, "a") { return "weak" }
if str_eq(gender, "neuter") { return "strong_neut" }
return "strong_masc"
}
// Strong masculine a-stem (cyning pattern)
//
// Stem: the noun as given (nom sg lacks an inflectional ending in this class).
//
// Singular: nom - acc - gen -es dat -e
// Plural: nom -as acc -as gen -a dat -um
fn ang_decline_strong_masc(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "es" }
if str_eq(gram_case, "dative") { return noun + "e" }
return noun
}
// plural
if str_eq(gram_case, "nominative") { return noun + "as" }
if str_eq(gram_case, "accusative") { return noun + "as" }
if str_eq(gram_case, "genitive") { return noun + "a" }
if str_eq(gram_case, "dative") { return noun + "um" }
return noun + "as"
}
// Strong neuter a-stem (word pattern)
//
// Singular: same as strong masc
// Plural: nom/acc - gen -a dat -um
fn ang_decline_strong_neut(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "es" }
if str_eq(gram_case, "dative") { return noun + "e" }
return noun
}
// plural: neuters have zero ending in nom/acc
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "a" }
if str_eq(gram_case, "dative") { return noun + "um" }
return noun
}
// Weak n-stem (nama pattern)
//
// The nom sg ends in -a; the oblique stem is formed by stripping -a and adding
// -an. Plural genitive is -ena.
//
// Singular: nom -a acc -an gen -an dat -an
// Plural: nom -an acc -an gen -ena dat -um
fn ang_decline_weak(noun: String, gram_case: String, number: String) -> String {
// Oblique stem: strip the final -a
let stem: String = ang_str_drop_last(noun, 1)
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return stem + "an" }
if str_eq(gram_case, "genitive") { return stem + "an" }
if str_eq(gram_case, "dative") { return stem + "an" }
return noun
}
// plural
if str_eq(gram_case, "nominative") { return stem + "an" }
if str_eq(gram_case, "accusative") { return stem + "an" }
if str_eq(gram_case, "genitive") { return stem + "ena" }
if str_eq(gram_case, "dative") { return stem + "um" }
return stem + "an"
}
// ang_decline: main declension entry point
//
// noun: nominative singular Old English noun (e.g. "cyning", "word", "nama")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// gender: "masculine" | "neuter" | "feminine" | "" (empty triggers inference)
//
// Returns the inflected form. Falls back to the nominative singular for any
// unrecognised combination.
fn ang_decline(noun: String, gram_case: String, number: String, gender: String) -> String {
let decl: String = ang_declension(noun, gender)
if str_eq(decl, "strong_masc") {
return ang_decline_strong_masc(noun, gram_case, number)
}
if str_eq(decl, "strong_neut") {
return ang_decline_strong_neut(noun, gram_case, number)
}
if str_eq(decl, "weak") {
return ang_decline_weak(noun, gram_case, number)
}
// Unknown: return nominative unchanged
return noun
}
// Definite article / demonstrative: se/sēo/þæt
//
// Old English used the demonstrative pronoun se/sēo/þæt as a definite article.
// The full paradigm (gender × case × number) is given below.
//
// Masculine:
// sg: nom se acc þone gen þæs dat þǣm
// pl: nom þā acc þā gen þāra dat þǣm
//
// Feminine:
// sg: nom sēo acc þā gen þǣre dat þǣre
// pl: nom þā acc þā gen þāra dat þǣm
//
// Neuter:
// sg: nom þæt acc þæt gen þæs dat þǣm
// pl: nom þā acc þā gen þāra dat þǣm
fn ang_article_masculine(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "se" }
if str_eq(gram_case, "accusative") { return "þone" }
if str_eq(gram_case, "genitive") { return "þæs" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "se"
}
// plural
if str_eq(gram_case, "nominative") { return "þā" }
if str_eq(gram_case, "accusative") { return "þā" }
if str_eq(gram_case, "genitive") { return "þāra" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "þā"
}
fn ang_article_feminine(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "sēo" }
if str_eq(gram_case, "accusative") { return "þā" }
if str_eq(gram_case, "genitive") { return "þǣre" }
if str_eq(gram_case, "dative") { return "þǣre" }
return "sēo"
}
// plural
if str_eq(gram_case, "nominative") { return "þā" }
if str_eq(gram_case, "accusative") { return "þā" }
if str_eq(gram_case, "genitive") { return "þāra" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "þā"
}
fn ang_article_neuter(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "þæt" }
if str_eq(gram_case, "accusative") { return "þæt" }
if str_eq(gram_case, "genitive") { return "þæs" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "þæt"
}
// plural
if str_eq(gram_case, "nominative") { return "þā" }
if str_eq(gram_case, "accusative") { return "þā" }
if str_eq(gram_case, "genitive") { return "þāra" }
if str_eq(gram_case, "dative") { return "þǣm" }
return "þā"
}
fn ang_article(gender: String, gram_case: String, number: String) -> String {
if str_eq(gender, "masculine") { return ang_article_masculine(gram_case, number) }
if str_eq(gender, "feminine") { return ang_article_feminine(gram_case, number) }
// neuter
return ang_article_neuter(gram_case, number)
}
// Gender inference from noun form
//
// A last-resort heuristic when the caller provides no gender hint.
// -a ending strongly suggests weak masculine or neuter (but most -a nouns are
// masculine weak). Without a full lexicon, masculine is the safe default.
fn ang_infer_gender(noun: String) -> String {
if ang_str_ends(noun, "u") { return "feminine" }
if ang_str_ends(noun, "e") { return "feminine" }
return "masculine"
}
// ang_noun_phrase: noun phrase builder
//
// Produces a declined noun with optional definite article (demonstrative)
// prepended. When gender is empty ("") it is inferred from the noun form.
//
// noun: nominative singular Old English noun
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" | "false"
fn ang_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let gender: String = ang_infer_gender(noun)
let declined: String = ang_decline(noun, gram_case, number, gender)
if str_eq(definite, "true") {
let art: String = ang_article(gender, gram_case, number)
return art + " " + declined
}
return declined
}

Some files were not shown because too many files have changed in this diff Show More