merge compiler features: break/continue, % modulo, for-range loops
This commit is contained in:
@@ -135,8 +135,9 @@ fn emit_blank() -> Void {
|
||||
fn binop_to_c(op: String) -> String {
|
||||
if op == "Plus" { return "+" }
|
||||
if op == "Minus" { return "-" }
|
||||
if op == "Star" { return "*" }
|
||||
if op == "Slash" { return "/" }
|
||||
if op == "Star" { return "*" }
|
||||
if op == "Slash" { return "/" }
|
||||
if op == "Percent" { return "%" }
|
||||
if op == "EqEq" { return "==" }
|
||||
if op == "NotEq" { return "!=" }
|
||||
if op == "Lt" { return "<" }
|
||||
@@ -1078,6 +1079,16 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "Break" {
|
||||
emit_line(indent + "break;")
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "Continue" {
|
||||
emit_line(indent + "continue;")
|
||||
return declared
|
||||
}
|
||||
|
||||
// Bare reassignment: `name = expr`. Always emits a plain C assignment
|
||||
// (no `el_val_t` prefix) - by construction the parser only produces
|
||||
// Assign for an existing identifier. If the name happens NOT to be in
|
||||
@@ -1130,6 +1141,28 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "ForRange" {
|
||||
let var_name: String = stmt["var"]
|
||||
let start_expr = stmt["start"]
|
||||
let end_expr = stmt["end"]
|
||||
let inclusive: Bool = stmt["inclusive"]
|
||||
let body = stmt["body"]
|
||||
let start_c: String = cg_expr(start_expr)
|
||||
let end_c: String = cg_expr(end_expr)
|
||||
// Loop variable introduced as a C local scoped to the for statement.
|
||||
// Body gets its own declared clone so let-bindings don't leak out.
|
||||
let body_decl = native_list_clone(declared)
|
||||
let body_decl = native_list_append(body_decl, var_name)
|
||||
if inclusive {
|
||||
emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " <= " + end_c + "; " + var_name + "++) {")
|
||||
} else {
|
||||
emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " < " + end_c + "; " + var_name + "++) {")
|
||||
}
|
||||
cg_stmts(body, indent + " ", body_decl)
|
||||
emit_line(indent + "}")
|
||||
return declared
|
||||
}
|
||||
|
||||
if kind == "FnDef" { return declared }
|
||||
if kind == "TypeDef" { return declared }
|
||||
if kind == "EnumDef" { return declared }
|
||||
@@ -1760,6 +1793,12 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
if str_eq(op, "Percent") {
|
||||
if is_int_expr(expr["left"]) {
|
||||
if is_int_expr(expr["right"]) { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
|
||||
+34
-16
@@ -1,4 +1,4 @@
|
||||
// lexer.el — el self-hosting lexer
|
||||
// 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:
|
||||
@@ -8,9 +8,9 @@
|
||||
// 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.
|
||||
// then indexes it with native_list_get - avoids O(N-) string cloning.
|
||||
|
||||
// ── Character helpers ─────────────────────────────────────────────────────────
|
||||
// -- Character helpers ---------------------------------------------------------
|
||||
|
||||
fn lex_is_digit(ch: String) -> Bool {
|
||||
if ch == "0" { return true }
|
||||
@@ -101,7 +101,7 @@ fn make_tok(kind: String, value: String) -> Map<String, Any> {
|
||||
{ "kind": kind, "value": value }
|
||||
}
|
||||
|
||||
// ── Keyword lookup ────────────────────────────────────────────────────────────
|
||||
// -- Keyword lookup ------------------------------------------------------------
|
||||
|
||||
fn keyword_kind(word: String) -> String {
|
||||
if word == "let" { return "Let" }
|
||||
@@ -147,13 +147,15 @@ fn keyword_kind(word: String) -> String {
|
||||
if word == "accessor" { return "Accessor" }
|
||||
if word == "vessel" { return "Vessel" }
|
||||
if word == "extern" { return "Extern" }
|
||||
if word == "break" { return "Break" }
|
||||
if word == "continue" { return "Continue" }
|
||||
""
|
||||
}
|
||||
|
||||
// ── Scan helpers ──────────────────────────────────────────────────────────────
|
||||
// -- Scan helpers --------------------------------------------------------------
|
||||
// All scan helpers receive the chars list and total length.
|
||||
|
||||
// scan_digits — advance i while chars[i] is a digit
|
||||
// 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
|
||||
@@ -175,7 +177,7 @@ fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// scan_ident — advance i while chars[i] is alphanumeric or underscore
|
||||
// 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()
|
||||
@@ -196,14 +198,14 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "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
|
||||
// -- 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
|
||||
// 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 {
|
||||
@@ -245,7 +247,7 @@ fn looks_like_code(s: String) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
// strip_code_comments — character-by-character walk. Tracks JS string state
|
||||
// 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
|
||||
@@ -398,7 +400,7 @@ fn strip_code_comments(s: String) -> String {
|
||||
str_join(out_parts, "")
|
||||
}
|
||||
|
||||
// scan_string — scan a quoted string literal, handling \" escapes.
|
||||
// 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
|
||||
@@ -458,7 +460,7 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "text": str_join(parts, ""), "pos": i }
|
||||
}
|
||||
|
||||
// ── Main lexer ────────────────────────────────────────────────────────────────
|
||||
// -- Main lexer ----------------------------------------------------------------
|
||||
|
||||
fn lex(source: String) -> [Map<String, Any>] {
|
||||
let chars: [String] = native_string_chars(source)
|
||||
@@ -696,8 +698,24 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "." {
|
||||
let tokens = native_list_append(tokens, make_tok("Dot", "."))
|
||||
let i = i + 1
|
||||
// Check for ..= (inclusive range) before .. (exclusive range) before single .
|
||||
let peek2_i = i + 2
|
||||
let peek2_ch = ""
|
||||
if peek2_i < total {
|
||||
let peek2_ch: String = native_list_get(chars, peek2_i)
|
||||
}
|
||||
if peek_ch == "." {
|
||||
if peek2_ch == "=" {
|
||||
let tokens = native_list_append(tokens, make_tok("DotDotEq", "..="))
|
||||
let i = i + 3
|
||||
} else {
|
||||
let tokens = native_list_append(tokens, make_tok("DotDot", ".."))
|
||||
let i = i + 2
|
||||
}
|
||||
} else {
|
||||
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", ";"))
|
||||
@@ -711,7 +729,7 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let tokens = native_list_append(tokens, make_tok("QuestionMark", "?"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
// unknown char — skip
|
||||
// unknown char - skip
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
+59
-21
@@ -1,4 +1,4 @@
|
||||
// parser.el — el self-hosting recursive descent parser
|
||||
// parser.el - el self-hosting recursive descent parser
|
||||
//
|
||||
// Consumes the token list produced by lexer.el and builds a list of AST
|
||||
// statement maps. Each statement and expression is a Map<String, Any>.
|
||||
@@ -11,7 +11,7 @@
|
||||
//
|
||||
// Entry point: fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>]
|
||||
|
||||
// ── Token access helpers ──────────────────────────────────────────────────────
|
||||
// -- Token access helpers ------------------------------------------------------
|
||||
|
||||
fn tok_at(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
native_list_get(tokens, pos)
|
||||
@@ -36,13 +36,13 @@ fn expect(tokens: [Map<String, Any>], pos: Int, kind: String) -> Int {
|
||||
pos + 1
|
||||
}
|
||||
|
||||
// ── Result helpers ────────────────────────────────────────────────────────────
|
||||
// -- Result helpers ------------------------------------------------------------
|
||||
|
||||
fn make_result(node: Map<String, Any>, pos: Int) -> Map<String, Any> {
|
||||
{ "node": node, "pos": pos }
|
||||
}
|
||||
|
||||
// ── Type annotation parser ────────────────────────────────────────────────────
|
||||
// -- Type annotation parser ----------------------------------------------------
|
||||
// Skips over a type annotation, returning the new position.
|
||||
// Types can be: Ident, [Type], Map<K,V>, Type?, Type<Type,...>
|
||||
|
||||
@@ -100,8 +100,8 @@ fn skip_type(tokens: [Map<String, Any>], pos: Int) -> Int {
|
||||
pos + 1
|
||||
}
|
||||
|
||||
// ── Parameter list ────────────────────────────────────────────────────────────
|
||||
// Parses (name: Type, name: Type, ...) — returns { "params": [...], "pos": ... }
|
||||
// -- Parameter list ------------------------------------------------------------
|
||||
// Parses (name: Type, name: Type, ...) - returns { "params": [...], "pos": ... }
|
||||
|
||||
fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let p = expect(tokens, pos, "LParen")
|
||||
@@ -140,7 +140,7 @@ fn parse_params(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
{ "params": params, "pos": p }
|
||||
}
|
||||
|
||||
// ── Expression parsing ────────────────────────────────────────────────────────
|
||||
// -- Expression parsing --------------------------------------------------------
|
||||
|
||||
fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let k = tok_kind(tokens, pos)
|
||||
@@ -212,14 +212,14 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
//
|
||||
// Suppression: when parse_if / parse_while / parse_for / parse_match
|
||||
// are parsing a head expression, they set __no_block_expr=1 so a stray
|
||||
// `{` here doesn't get gobbled as a Map literal — it belongs to the
|
||||
// `{` here doesn't get gobbled as a Map literal - it belongs to the
|
||||
// following block. Without this, `if a || b { ... }` could mis-parse
|
||||
// (the `||` recursion lands at `{` and tries to read the if-body as a
|
||||
// Map, then loops forever when keys don't match `Str: expr`).
|
||||
if k == "LBrace" {
|
||||
let no_block: String = state_get("__no_block_expr")
|
||||
if str_eq(no_block, "1") {
|
||||
// Fall through to fallback — caller will see `{` and treat it
|
||||
// Fall through to fallback - caller will see `{` and treat it
|
||||
// as the start of the block they're expecting.
|
||||
return make_result({ "expr": "Nil" }, pos)
|
||||
}
|
||||
@@ -300,7 +300,7 @@ fn parse_primary(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
// token kinds for the deploy/retry DSLs, but they're also valid as
|
||||
// parameter names and local variables. When one of these appears in
|
||||
// expression position (where only an Ident makes sense), treat it as
|
||||
// an Ident carrying the original text — otherwise references to a
|
||||
// an Ident carrying the original text - otherwise references to a
|
||||
// parameter named `target` compile to EL_NULL.
|
||||
if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
||||
if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) }
|
||||
@@ -472,9 +472,9 @@ fn parse_block(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
{ "stmts": stmts, "pos": p }
|
||||
}
|
||||
|
||||
// ── Postfix expressions (calls, field access, index) ─────────────────────────
|
||||
// -- Postfix expressions (calls, field access, index) -------------------------
|
||||
|
||||
// is_duration_unit — recognise the postfix unit suffix on a numeric literal.
|
||||
// is_duration_unit - recognise the postfix unit suffix on a numeric literal.
|
||||
// Used by parse_postfix to detect `30.seconds`-shape time literals before
|
||||
// falling back to the generic `obj.field` field-access lowering. Singular
|
||||
// and plural forms map to the same nanosecond multiplier; codegen does the
|
||||
@@ -578,7 +578,7 @@ fn parse_postfix(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
make_result(node, p)
|
||||
}
|
||||
|
||||
// ── Binary expression precedence climbing ────────────────────────────────────
|
||||
// -- Binary expression precedence climbing ------------------------------------
|
||||
|
||||
fn op_precedence(kind: String) -> Int {
|
||||
if kind == "Or" { return 1 }
|
||||
@@ -593,6 +593,7 @@ fn op_precedence(kind: String) -> Int {
|
||||
if kind == "Minus" { return 5 }
|
||||
if kind == "Star" { return 6 }
|
||||
if kind == "Slash" { return 6 }
|
||||
if kind == "Percent" { return 6 }
|
||||
0
|
||||
}
|
||||
|
||||
@@ -609,6 +610,7 @@ fn is_binop(kind: String) -> Bool {
|
||||
if kind == "Minus" { return true }
|
||||
if kind == "Star" { return true }
|
||||
if kind == "Slash" { return true }
|
||||
if kind == "Percent" { return true }
|
||||
false
|
||||
}
|
||||
|
||||
@@ -641,7 +643,7 @@ fn parse_expr(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
parse_binop(tokens, pos, 1)
|
||||
}
|
||||
|
||||
// ── Statement parsing ─────────────────────────────────────────────────────────
|
||||
// -- Statement parsing ---------------------------------------------------------
|
||||
|
||||
fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let k = tok_kind(tokens, pos)
|
||||
@@ -653,7 +655,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
let p = p + 1
|
||||
let ltype = ""
|
||||
let k2 = tok_kind(tokens, p)
|
||||
// optional type annotation: name: Type — capture the leading
|
||||
// optional type annotation: name: Type - capture the leading
|
||||
// identifier so codegen can dispatch arithmetic vs concat on
|
||||
// `+` between two typed Idents.
|
||||
if k2 == "Colon" {
|
||||
@@ -687,7 +689,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "stmt": "Return", "value": val }, p)
|
||||
}
|
||||
|
||||
// extern fn declaration (no body — forward declaration for separate compilation)
|
||||
// extern fn declaration (no body - forward declaration for separate compilation)
|
||||
if k == "Extern" {
|
||||
let p = pos + 1
|
||||
let k2: String = tok_kind(tokens, p)
|
||||
@@ -858,7 +860,17 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
return make_result({ "stmt": "While", "cond": cond, "body": body }, p)
|
||||
}
|
||||
|
||||
// for loop
|
||||
// break statement
|
||||
if k == "Break" {
|
||||
return make_result({ "stmt": "Break" }, pos + 1)
|
||||
}
|
||||
|
||||
// continue statement
|
||||
if k == "Continue" {
|
||||
return make_result({ "stmt": "Continue" }, pos + 1)
|
||||
}
|
||||
|
||||
// for loop (range or list iteration)
|
||||
if k == "For" {
|
||||
let p = pos + 1
|
||||
let item_name = tok_value(tokens, p)
|
||||
@@ -868,15 +880,41 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
state_set("__no_block_expr", "1")
|
||||
let r = parse_expr(tokens, p)
|
||||
state_set("__no_block_expr", prev_no_block)
|
||||
let list_expr = r["node"]
|
||||
let start_expr = r["node"]
|
||||
let p = r["pos"]
|
||||
// Check for range operator: .. (exclusive) or ..= (inclusive)
|
||||
let range_k = tok_kind(tokens, p)
|
||||
if range_k == "DotDot" {
|
||||
// exclusive range: for i in start..end
|
||||
let p = p + 1
|
||||
let r2 = parse_expr(tokens, p)
|
||||
let end_expr = r2["node"]
|
||||
let p = r2["pos"]
|
||||
let r3 = parse_block(tokens, p)
|
||||
let body = r3["stmts"]
|
||||
let p = r3["pos"]
|
||||
return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": false, "body": body }, p)
|
||||
}
|
||||
if range_k == "DotDotEq" {
|
||||
// inclusive range: for i in start..=end
|
||||
let p = p + 1
|
||||
let r2 = parse_expr(tokens, p)
|
||||
let end_expr = r2["node"]
|
||||
let p = r2["pos"]
|
||||
let r3 = parse_block(tokens, p)
|
||||
let body = r3["stmts"]
|
||||
let p = r3["pos"]
|
||||
return make_result({ "stmt": "ForRange", "var": item_name, "start": start_expr, "end": end_expr, "inclusive": true, "body": body }, p)
|
||||
}
|
||||
// No range operator: regular for-in (list iteration)
|
||||
let list_expr = start_expr
|
||||
let r2 = parse_block(tokens, p)
|
||||
let body = r2["stmts"]
|
||||
let p = r2["pos"]
|
||||
return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p)
|
||||
}
|
||||
|
||||
// @decorator — capture decorator name and attach to following stmt
|
||||
// @decorator - capture decorator name and attach to following stmt
|
||||
if k == "At" {
|
||||
let p = pos + 1
|
||||
let dec_name = tok_value(tokens, p)
|
||||
@@ -1039,7 +1077,7 @@ fn parse_stmt(tokens: [Map<String, Any>], pos: Int) -> Map<String, Any> {
|
||||
make_result({ "stmt": "Expr", "value": val }, p)
|
||||
}
|
||||
|
||||
// ── Top-level parse ────────────────────────────────────────────────────────────
|
||||
// -- Top-level parse ------------------------------------------------------------
|
||||
|
||||
fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let total: Int = native_list_len(tokens)
|
||||
@@ -1058,7 +1096,7 @@ fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
|
||||
let stmt = r["node"]
|
||||
let new_pos: Int = r["pos"]
|
||||
let stmts = native_list_append(stmts, stmt)
|
||||
// Guard against infinite loops — if pos didn't advance, force it
|
||||
// Guard against infinite loops - if pos didn't advance, force it
|
||||
if new_pos <= pos {
|
||||
let pos = pos + 1
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user