merge compiler features: break/continue, % modulo, for-range loops

This commit is contained in:
Will Anderson
2026-05-03 15:45:45 -05:00
3 changed files with 134 additions and 39 deletions
+41 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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 {